Tuesday, August 13, 2013

Android and Autocomplete


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);
  ArrayAdapter array=new ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, TEXTS);
  textView.setAdapter(array);
 }
And main layout

 


    


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 ArrayAdapter {
 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;
 }
}
Now a little change in my Activity onCreate to pass my Adapter and layout
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  ArrayList 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);
 }
This gave me following output, which is pretty decent i guess.

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.
  1.  Change the image on the Adapter Side 
  2.  Pass an Object to the adapter which will contain every single information needed to draw a row.
 Second way seems to be more cleaner approach, as my adapter code will remain will be readable and in that case i just need to update my adapter to understand a custom object which will represent my row information and so i decided to take this route.

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);
  ArrayList data=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 ArrayAdapter implements 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();
           }  
       }
  };
}
Loading data from SQLite Database

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.
  1. Create the database
  2. Insert data into database
  3. Query the database
  4. Create my adapter to show and display data.
  5. 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.

  1. 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. 
  2. 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:

Anonymous said...

Thank you very much it helped me a lot !

Anonymous said...

i need this code.please send me.mail:loganathantk@gmail.com