Using Array Adapter
Couple of days ago my boss asked me to add Auto Complete in a search field. Actually he was impressed by seeing Google search bar.
So I start looking into the this issue and find out steps are quite simple.
1. Use AutoCompleteEditTextView
2. Set ArrayAdapter
So just by added following code on Activity onCreate(),
private final String[] TEXTS = new String[] { "Google Mail", "Google Search", "Google Plus", "Google Finance", "Google Docs", "Google Drive" }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); AutoCompleteTextView textView = (AutoCompleteTextView) findViewById(R.id.auto_complete_tv); ArrayAdapterAnd main layoutarray=new ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, TEXTS); textView.setAdapter(array); }
This eventually gave me following output
. FYI,
android.R.layout.simple_dropdown_item_1line is provided by android platform.
Custom Array Adapter
Thats a good start but i do not have image on right side. So first thing i need a custom layout to show my custom styled row.
And, my adapter need to understand my new layout that also means that i have to change my ArrayAdapter.
I extend default ArrayAdapter and just update getView method.
public class AutoCompleteSimpleArrayAdapter extends ArrayAdapterNow a little change in my Activity onCreate to pass my Adapter and layout{ private final Context mContext; private final int layoutId; ArrayList data_array; public AutoCompleteSimpleArrayAdapter(Context context, int resource, ArrayList objects) { super(context, resource, objects); // TODO Auto-generated constructor stub this.data_array = objects; this.mContext=context; this.layoutId=resource; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { LayoutInflater vi =(LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = vi.inflate(layoutId, null); } TextView tv=(TextView)convertView.findViewById(R.id.auto_compete_textView); tv.setText(data_array.get(position)); return convertView; } }
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ArrayListThis gave me following output, which is pretty decent i guess.data=new ArrayList(); data.add("Google Mail"); data.add("Google Search"); data.add("Google Plus"); data.add("Google Finance"); data.add("Google Docs"); data.add( "Google Drive"); AutoCompleteTextView textView = (AutoCompleteTextView) findViewById(R.id.auto_complete_tv); AutoCompleteSimpleArrayAdapter adapter=new AutoCompleteSimpleArrayAdapter(this, R.layout.simple_image_autocomplete, data); textView.setAdapter(adapter); }
Go fancy with layout
But when i was showing this to our designers, she seems not very impressed with one static icon and asked me if we can do little dynamic image and change the images based on the text. And, when start thinking about the problem that seems like whole new issue. I figured out that, in two way i can resolve the issue.
- Change the image on the Adapter Side
- Pass an Object to the adapter which will contain every single information needed to draw a row.
This my simple POJO, to represent two images and one text.
public class AutoCompleteRow { private int logo; private String text; private int actionImage; AutoCompleteRow(int logo, String text, int actionImage){ this.logo=logo; this.text=text; this.actionImage=actionImage; } public int getLogo() { return logo; } public void setLogo(int logo) { this.logo = logo; } public String getText() { return text; } public void setText(String text) { this.text = text; } public int getActionImage() { return actionImage; } public void setActionImage(int actionImage) { this.actionImage = actionImage; } }Made new layout name auto_compete_row_items.xml when one image is on left one is on right and text is in next to the left image.
Now rewrite my Adapter to take this new change
public class AutoCompleteArrayAdapter extends ArrayAdapter{ protected static final String TAG = AutoCompleteArrayAdapter.class.getSimpleName(); private final Context mContext; private final int layoutId; private ArrayList data_array; public AutoCompleteArrayAdapter(Context context, int textViewResourceId, ArrayList entries) { super(context, textViewResourceId, entries); this.data_array = entries; this.mContext=context; this.layoutId=textViewResourceId; } public AutoCompleteRow getItem (int position){ return this.data_array.get(position); } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { LayoutInflater vi =(LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = vi.inflate(layoutId, null); } TextView tv=(TextView)convertView.findViewById(R.id.auto_compete_textView); tv.setText(data_array.get(position).getText()); ImageView logo=(ImageView)convertView.findViewById(R.id.image_view_logo); logo.setImageResource(data_array.get(position).getLogo()); ImageView action=(ImageView)convertView.findViewById(R.id.image_view_action); action.setImageResource(data_array.get(position).getActionImage()); return convertView; } }
Finaly, updated Activity on create
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ArrayListdata=new ArrayList (); data.add(new AutoCompleteRow(R.drawable.google, "Google Mail", R.drawable.gmail)); data.add(new AutoCompleteRow(R.drawable.google, "Google Plus", R.drawable.google_plus)); data.add(new AutoCompleteRow(R.drawable.google, "Google Search", R.drawable.search)); AutoCompleteTextView textView = (AutoCompleteTextView) findViewById(R.id.auto_complete_tv); AutoCompleteArrayAdapter adapter=new AutoCompleteArrayAdapter(this, R.layout.auto_compete_row_items, data); textView.setAdapter(adapter); }
Customizing Search Option
When i run this program, i end up with no output. After spending couple of hours trying to debug the issue i realize that, i am using custom object and i have to override getFilter() from my Adapter class. Because AutoCompleteTextView dosnt know how to filter and display text suggestions. I even customize my filter to show me auto-complete hints if database character contains typed characters instead of typical start with. After running every thing together i got following output.
if you see the out put, i typed "le", and my auto complete generated all string contains with "le".
This is final Adapter code everything together.
public class AutoCompleteArrayAdapter extends ArrayAdapterLoading data from SQLite Databaseimplements Filterable{ protected static final String TAG = AutoCompleteArrayAdapter.class.getSimpleName(); private final Context mContext; private final int layoutId; private ArrayList data_array; public AutoCompleteArrayAdapter(Context context, int textViewResourceId, ArrayList entries) { super(context, textViewResourceId, entries); this.data_array = entries; this.mContext=context; this.layoutId=textViewResourceId; } public AutoCompleteRow getItem (int position){ return this.data_array.get(position); } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { LayoutInflater vi =(LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = vi.inflate(layoutId, null); } TextView tv=(TextView)convertView.findViewById(R.id.auto_compete_textView); tv.setText(data_array.get(position).getText()); ImageView logo=(ImageView)convertView.findViewById(R.id.image_view_logo); logo.setImageResource(data_array.get(position).getLogo()); ImageView action=(ImageView)convertView.findViewById(R.id.image_view_action); action.setImageResource(data_array.get(position).getActionImage()); return convertView; } @Override public Filter getFilter() { return myFilter; } Filter myFilter = new Filter() { @Override protected FilterResults performFiltering(CharSequence constraint) { FilterResults filterResults = new FilterResults(); ArrayList orig_array=new ArrayList (); if(constraint != null && data_array!=null) { int length=data_array.size(); int i=0; while(i ) results.values; if (results.count > 0) { notifyDataSetChanged(); } else { notifyDataSetInvalidated(); } } }; }
Our designer told me yes that is what she wants and . So everything is great, but after couple of days another engineers telling me we have more then thousands String in auto-complete suggestion. This raises new issue that we can not store all these text on the memory anymore. So we have to persist it. But, File base I/O do not seems like a good option that means i have to find a way retrieve those word from SQLite database.
After doing some investigation i came up with following steps to resolve the issue.
- Create the database
- Insert data into database
- Query the database
- Create my adapter to show and display data.
- Bind things together.
For creating my database i can use SQLiteOpenHelper.
public class AutoCompleteHelper extends SQLiteOpenHelper { private static final String TAG = AutoCompleteHelper.class.getSimpleName(); public static final String PRODUCT_TABLE = "_product"; public static final int DB_VERSION = 1; private static final String DATABASE_NAME = " auto_complete.db"; public AutoCompleteHelper(Context context) { super(context, DATABASE_NAME, null, DB_VERSION); } public Cursor query(SQLiteDatabase db, String query) { Cursor cursor = db.rawQuery(query, null); return cursor; } @Override public void onCreate(SQLiteDatabase db) { // TODO Auto-generated method stub final String create_sql_product=String.format("CREATE TABLE %s (" + " %s INTEGER PRIMARY KEY AUTOINCREMENT," + " %s CHAR(255));", PRODUCT_TABLE, ProductsTable._ID, ProductsTable.MODEL); db.execSQL(create_sql_product); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // TODO Auto-generated method stub db.execSQL(String.format("DROP TABLE IF EXISTS %s", PRODUCT_TABLE)); this.onCreate(db); } public static class ProductsTable implements BaseColumns{ public static final String MODEL="_model"; } }
I am implementing base columns to avoid creating required android fields by my self. Other then that table contain only one _model fields.
So far everything is ok. But i have couple of issue.
- If i am inserting data on database in Activty onCreate() method i can not insert new data, every time my activity gets created. That means i need a way to resolve this.
- I also have to do regular Query and return cursor (Reason we will see later).
For resolving both problems, i have created another abstraction which is my Data Access Object (DAO) layer. That means when ever i need data i will ask to DAO and DAO will talk with Database directly.
public class AutoCompleteDAO { private final Context mContext; private static final String[] DATA = new String[] { "Google Mail", "Google Search", "Google Plus", "Google Finance", "Google Docs", "Google Drive" }; private final AutoCompleteHelper mDataBaseHelper; public AutoCompleteDAO(Context context){ mContext=context; mDataBaseHelper=new AutoCompleteHelper(mContext); if (!isDataExist()) addToDatabase(DATA); } public boolean isDataExist(){ SQLiteDatabase database=mDataBaseHelper.getReadableDatabase(); long rows; SQLiteStatement s = database.compileStatement("select count(*) from _product;"); try{ rows= s.simpleQueryForLong(); }catch(Exception e){ e.printStackTrace(); return false; } return (rows>0) ? true:false; } public Cursor getAllData() { String selectQuery = "SELECT * FROM _product"; SQLiteDatabase db = mDataBaseHelper.getReadableDatabase(); Cursor cursor = db.rawQuery(selectQuery, null); return cursor; } public Cursor getModelCursor(CharSequence args){ SQLiteDatabase database=mDataBaseHelper.getReadableDatabase(); String sqlQuery = ""; Cursor result = null; sqlQuery = " SELECT _id, _model"; sqlQuery += " FROM "+AutoCompleteHelper.PRODUCT_TABLE; sqlQuery += " WHERE _model LIKE '%" + args + "%' "; sqlQuery += " ORDER BY _model;"; result=database.rawQuery(sqlQuery, null); return result; } public void addToDatabase(String name){ ContentValues values = new ContentValues(); values.put(ProductsTable.MODEL, name); SQLiteDatabase database=mDataBaseHelper.getWritableDatabase(); database.beginTransaction(); database.insert(AutoCompleteHelper.PRODUCT_TABLE, null, values); database.setTransactionSuccessful(); database.endTransaction(); database.close(); } public boolean addToDatabase(String... models){ ContentValues values = new ContentValues(); SQLiteDatabase database=mDataBaseHelper.getWritableDatabase(); database.beginTransaction(); try{ for (String model:models){ values.put(ProductsTable.MODEL, model); database.insert(AutoCompleteHelper.PRODUCT_TABLE, null, values); } database.setTransactionSuccessful(); }catch (SQLException e) { return false; } finally { database.endTransaction(); database.close(); } return true; } }
Couple of things in here
addToDatabase: for inserting data into database i created this overloaded method to insert one single item or arrays.
getModelCursor(CharSequence seq): This method take a String parameter and Query the database and return whatever the data matches parameter string. You will probably already notice, my query string contains LIKE '%" .
getAllData: It simply returrn result set of all data.
isDataExist(): this where i am checking how many row i have in my database. I decided to do this way it is because i thought i might add more tables in my database.
Now i have everything i need to create my adapter
public class AutoCompeteAdapter extends CursorAdapter { private static final String TAG=AutoCompeteAdapter.class.getSimpleName(); private final Context mContext; private AutoCompleteDAO dataBaseHelper; public AutoCompeteAdapter(Context context, Cursor cursor, boolean autoRequery) { super(context, cursor, autoRequery); mContext=context; dataBaseHelper=new AutoCompleteDAO(context); } @Override public void bindView(View view, Context context, Cursor cursor) { // TODO Auto-generated method stub TextView tv=(TextView)view.findViewById(R.id.auto_compete_textView); String text=cursor.getString(1); tv.setText(text); } @Override public View newView(Context context, Cursor cursor, ViewGroup arg2) { // TODO Auto-generated method stub LayoutInflater vi =(LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View convertView = vi.inflate(R.layout.simple_image_autocomplete, null); return convertView; } @Override public Cursor runQueryOnBackgroundThread(CharSequence constraint){ Cursor cursor = dataBaseHelper.getModelCursor(constraint); return cursor; } @Override public String convertToString(Cursor cursor) { return cursor.getString(cursor.getColumnIndex(ProductsTable.MODEL)); } }
convertToString: This function is needed when we select an auto-complete item from the drop down. Otherwise auto complete text will filled with cursor objects toString() method.
runQueryOnBackgroundThread: As this is a non UI thread function i am just making sure what query will run and what will be returned is implemented in the DAO.
newView: is for creating the view for the first time. I am using one of my layout i used earlier. Important things to notice here is, i do not have to check if convert view is null as it is done by the system and i have bindView method on cursor adapter.
my layout is simple_image_auto_complete.xml
My on create now look like this,
And, finally when i run everything together, i got following output.
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); AutoCompleteTextView textView = (AutoCompleteTextView) findViewById(R.id.auto_complete_tv); Cursor cursor=new AutoCompleteDAO(this).getAllData(); AutoCompeteAdapter adapter=new AutoCompeteAdapter(this,cursor , true); textView.setAdapter(adapter); }
Voilla!!!!
2 comments:
Thank you very much it helped me a lot !
i need this code.please send me.mail:loganathantk@gmail.com
Post a Comment