Sunday 8 April 2012

Adding CheckBoxes to a Custom ListView in Android

 Adding checkbox to a Custom ListView..sounds simple right?? Add a CheckBox to the layout,inflate,add to the ListView and everything's fine UNTIL you start scrolling!! Try checking a CheckBox and scroll down.You will find many more checked ones down below. Try scrollig up again and you will find even more!!! You will soon realize that the more you scroll,the CheckBoxes get jumbled up even more!! Wondering why?? The troublemaker here is convertView,which the ListView re-uses as you scroll. When you check a CheckBox,every list item that uses that specific convertView will have a checked CheckBox.

So how do you get around this??
Simple.Have a cache which saves the state of every CheckBox in the ListView.
Since a CheckBox can have only two states,checked or unchecked,a simple boolean array
will serve the purpose.

In effect,whenever a CheckBox in the ListView is checked or unchecked,its resulting state is stored in the boolean array.When the user scrolls,this boolean array is used to set the state of the CheckBox.

This is my ListView:


The layout for the list item is:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <CheckBox 
        android:id="@+id/checkBox"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        />
    
    
    <ImageView 
        android:id="@+id/photo"
        android:layout_width="55dp"
        android:layout_height="70dp"
        android:layout_toRightOf="@id/checkBox"
        android:scaleType="fitXY"/>
    
    <TextView 
        android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/photo"
        android:layout_alignTop="@id/photo"
        android:textSize="18sp"/>
    
    <TextView 
        android:id="@+id/team"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/photo"
        android:layout_below="@id/name"/>
    
    
    
</RelativeLayout>

My main.xml for the ListActivity:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

<ListView 
        android:id="@android:id/list"
        android:layout_width="fill_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>
<ImageView android:id="@android:id/empty"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:src="@drawable/no_player"/>
</LinearLayout>

The CustomAdapter is where we declare the boolean array for holding the CheckBox states.
We also add an onClickListener to the CheckBox. When the user clicks a CheckBox,
if it is checked,then the listener sets the corresponding boolean array to true.Else,it is
set to false.
This boolean array sets the state of the CheckBox using the setChecked(boolean) method.

This is the CustomAdapter I used:

//define your custom adapter
private class CustomAdapter extends ArrayAdapter<HashMap<String, Object>>
{
   // boolean array for storing
   //the state of each CheckBox 
   boolean[] checkBoxState;
  
 
   ViewHolder viewHolder;
  
   public CustomAdapter(Context context, int textViewResourceId,
   ArrayList<HashMap<String, Object>> players) {

    //let android do the initializing :)
    super(context, textViewResourceId, players); 
   
  //create the boolean array with
   //initial state as false
  checkBoxState=new boolean[players.size()];
  }


    //class for caching the views in a row  
 private class ViewHolder
 {
   ImageView photo;
   TextView name,team;
   CheckBox checkBox;
 }

  

 @Override
 public View getView(final int position, View convertView, ViewGroup parent) {

   if(convertView==null)
    {
   convertView=inflater.inflate(R.layout.players_layout, null);
   viewHolder=new ViewHolder();

    //cache the views
    viewHolder.photo=(ImageView) convertView.findViewById(R.id.photo);
    viewHolder.name=(TextView) convertView.findViewById(R.id.name);
    viewHolder.team=(TextView) convertView.findViewById(R.id.team);
    viewHolder.checkBox=(CheckBox) convertView.findViewById(R.id.checkBox);

     //link the cached views to the convertview
    convertView.setTag( viewHolder);
   

  }
  else
   viewHolder=(ViewHolder) convertView.getTag();

           
  int photoId=(Integer) players.get(position).get("photo");

  //set the data to be displayed
  viewHolder.photo.setImageDrawable(getResources().getDrawable(photoId));
  viewHolder.name.setText(players.get(position).get("name").toString());
  viewHolder.team.setText(players.get(position).get("team").toString());
   
   //VITAL PART!!! Set the state of the 
   //CheckBox using the boolean array
        viewHolder.checkBox.setChecked(checkBoxState[position]);
            
         
           //for managing the state of the boolean
           //array according to the state of the
           //CheckBox
          
           viewHolder.checkBox.setOnClickListener(new View.OnClickListener() {
    
   public void onClick(View v) {
    if(((CheckBox)v).isChecked())
     checkBoxState[position]=true;
    else
     checkBoxState[position]=false;
     
    }
   });

   //return the view to be displayed
   return convertView;
  }

 }



Note lines 65 and 72.
In line 65,the state of the CheckBox is set to its correct state using the boolean array.
Line 72 alters the boolean array.When the user clicks on a CheckBox,the resulting state of that CheckBox is cached in the boolean array.This cached state is the one that is used to set the CheckBox to its correct state in line 65.

And this is my ListActivity  class by the way:

public class CustomListViewWithCheckBox extends ListActivity {


  //ArrayList that will hold the original Data
  ArrayList<HashMap<String, Object>> players;
  LayoutInflater inflater;

  @Override
  public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 
 

 //get the LayoutInflater for inflating the customomView
 //this will be used in the custom adapter
 inflater=(LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);

 //these arrays are just the data that 
 //I'll be using to populate the ArrayList
 //You can use our own methods to get the data
 String names[]={"Ronaldo","Messi","Torres","Iniesta",
    "Drogba","Gerrard","Rooney","Xavi"}; 

 String teams[]={"Real Madrid","Barcelona","Chelsea",
    "Barcelona","Chelsea","Liverpool",
    "ManU","Barcelona"};
 Integer[] photos={R.drawable.cr7,R.drawable.messi,
    R.drawable.torres,R.drawable.iniesta,
    R.drawable.drogba,R.drawable.gerrard,
    R.drawable.rooney,R.drawable.xavi};

 players=new ArrayList<HashMap<String,Object>>();

 //temporary HashMap for populating the 
 //Items in the ListView
 HashMap<String , Object> temp;

 //total number of rows in the ListView
 int noOfPlayers=names.length;

 //now populate the ArrayList players
 for(int i=0;i<noOfPlayers;i++)
 {
  temp=new HashMap<String, Object>();

  temp.put("name", names[i]);
  temp.put("team", teams[i]);    
  temp.put("photo", photos[i]);

  //add the row to the ArrayList
  players.add(temp);        
 }

 /*create the adapter
 *first param-the context
 *second param-the id of the layout file 
  you will be using to fill a row
 *third param-the set of values that
   will populate the ListView */
 final CustomAdapter adapter=new CustomAdapter(this, R.layout.players_layout,players); 
 
 //finally,set the adapter to the default ListView
 setListAdapter(adapter);
 
 
 }
This doesn't need any explanation I suppose.If you do,you are always welcome to check out
this post of mine..:).

Summing up,this is how it looks like:

public class CustomListViewWithCheckBox extends ListActivity {


  //ArrayList that will hold the original Data
  ArrayList<HashMap<String, Object>> players;
  LayoutInflater inflater;

  @Override
  public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 
 

 //get the LayoutInflater for inflating the customomView
 //this will be used in the custom adapter
 inflater=(LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);

 //these arrays are just the data that 
 //I'll be using to populate the ArrayList
 //You can use our own methods to get the data
 String names[]={"Ronaldo","Messi","Torres","Iniesta",
    "Drogba","Gerrard","Rooney","Xavi"}; 

 String teams[]={"Real Madrid","Barcelona","Chelsea",
    "Barcelona","Chelsea","Liverpool",
    "ManU","Barcelona"};
 Integer[] photos={R.drawable.cr7,R.drawable.messi,
    R.drawable.torres,R.drawable.iniesta,
    R.drawable.drogba,R.drawable.gerrard,
    R.drawable.rooney,R.drawable.xavi};

 players=new ArrayList<HashMap<String,Object>>();

 //temporary HashMap for populating the 
 //Items in the ListView
 HashMap<String , Object> temp;

 //total number of rows in the ListView
 int noOfPlayers=names.length;

 //now populate the ArrayList players
 for(int i=0;i<noOfPlayers;i++)
 {
  temp=new HashMap<String, Object>();

  temp.put("name", names[i]);
  temp.put("team", teams[i]);    
  temp.put("photo", photos[i]);

  //add the row to the ArrayList
  players.add(temp);        
 }

 /*create the adapter
 *first param-the context
 *second param-the id of the layout file 
  you will be using to fill a row
 *third param-the set of values that
   will populate the ListView */
 final CustomAdapter adapter=new CustomAdapter(this, R.layout.players_layout,players); 
 
 //finally,set the adapter to the default ListView
 setListAdapter(adapter);
 
 
 }


 //define your custom adapter
private class CustomAdapter extends ArrayAdapter<HashMap<String, Object>>
{
   // boolean array for storing
   //the state of each CheckBox 
   boolean[] checkBoxState;
  
 
   ViewHolder viewHolder;
  
   public CustomAdapter(Context context, int textViewResourceId,
   ArrayList<HashMap<String, Object>> players) {

    //let android do the initializing :)
    super(context, textViewResourceId, players); 
   
  //create the boolean array with
   //initial state as false
  checkBoxState=new boolean[players.size()];
  }


    //class for caching the views in a row  
 private class ViewHolder
 {
   ImageView photo;
   TextView name,team;
   CheckBox checkBox;
 }

  

 @Override
 public View getView(final int position, View convertView, ViewGroup parent) {

   if(convertView==null)
    {
   convertView=inflater.inflate(R.layout.players_layout, null);
   viewHolder=new ViewHolder();

    //cache the views
    viewHolder.photo=(ImageView) convertView.findViewById(R.id.photo);
    viewHolder.name=(TextView) convertView.findViewById(R.id.name);
    viewHolder.team=(TextView) convertView.findViewById(R.id.team);
    viewHolder.checkBox=(CheckBox) convertView.findViewById(R.id.checkBox);

     //link the cached views to the convertview
    convertView.setTag( viewHolder);
   

  }
  else
   viewHolder=(ViewHolder) convertView.getTag();

           
  int photoId=(Integer) players.get(position).get("photo");

  //set the data to be displayed
  viewHolder.photo.setImageDrawable(getResources().getDrawable(photoId));
  viewHolder.name.setText(players.get(position).get("name").toString());
  viewHolder.team.setText(players.get(position).get("team").toString());
   
   //VITAL PART!!! Set the state of the 
   //CheckBox using the boolean array
        viewHolder.checkBox.setChecked(checkBoxState[position]);
            
         
           //for managing the state of the boolean
           //array according to the state of the
           //CheckBox
          
           viewHolder.checkBox.setOnClickListener(new View.OnClickListener() {
    
   public void onClick(View v) {
    if(((CheckBox)v).isChecked())
     checkBoxState[position]=true;
    else
     checkBoxState[position]=false;
     
    }
   });

   //return the view to be displayed
   return convertView;
  }

 }
}
That's it.Happy coding.:)

Note: The setOnItemClickListener() may not fire when a 'clickable'  widget like a Button or CheckBox is part of a list item.
         To resolve this,simply set
         android:focusable="false"
         android:focusableInTouchMode="false"
        to the clickable widget.

Search a Custom ListView in Android

In my previous post , we implemented a Custom ListView.
Now,we'll see how to implement a search function in it.The default text filter associated with the ListView tends to malfunction when used with custom list items.There is a workaround to this,which I'll be posting soon.
 For now,we'll see how we can search the ListView under the current circumstances.

The idea is to have an EditText above the ListView and when the user types a search query,the ListView filters itself to show only the relevant results;Like this:







Here's the main.xml that shows the EditText along with the ListView:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    
    
<EditText  
    android:id="@+id/searchBox"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content"  />

<ListView 
        android:id="@android:id/list"
        android:layout_width="fill_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>
<ImageView android:id="@android:id/empty"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:src="@drawable/no_cocktail"/>

</LinearLayout>

Take note of the Id's used for the ListView and ImageView.They are @android:id/list and @android:id/empty respectively.Assigning these pre-defined IDs allows us to control what is shown to the user under different conditions,without much effort.ie,When the ListView contains items,the user will see the EditText and the ListView.When the list is empty,the user will see the EditText and the ImageView.As shown above,there is no player in the list whose name starts with 'h'.Hence,The ImageView with Id @android:id/empty is displayed.This is possible only because we are using a ListActivity.This method won't work in an Activity.
        Let's move on to the coding now.Here's the ListActivity.The onCreate() method is used here to populate the ArrayList of players.
public class SearchCustomListViewActivity extends ListActivity {

  //ArrayList thats going to hold the search results
  ArrayList<HashMap<String, Object>> searchResults;

  //ArrayList that will hold the original Data
  ArrayList<HashMap<String, Object>> originalValues;
  LayoutInflater inflater;

  @Override
  public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 
 setContentView(R.layout.main);
 final EditText searchBox=(EditText) findViewById(R.id.searchBox);
 ListView playersListView=(ListView) findViewById(android.R.id.list);

 //get the LayoutInflater for inflating the customomView
 //this will be used in the custom adapter
 inflater=(LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);

 //these arrays are just the data that 
 //I'll be using to populate the ArrayList
 //You can use our own methods to get the data
 String names[]={"Ronaldo","Messi","Torres","Iniesta",
    "Drogba","Gerrard","Rooney","Xavi"}; 

 String teams[]={"Real Madrid","Barcelona","Chelsea",
    "Barcelona","Chelsea","Liverpool",
    "ManU","Barcelona"};
 Integer[] photos={R.drawable.cr7,R.drawable.messi,
    R.drawable.torres,R.drawable.iniesta,
    R.drawable.drogba,R.drawable.gerrard,
    R.drawable.rooney,R.drawable.xavi};

 originalValues=new ArrayList<HashMap<String,Object>>();

 //temporary HashMap for populating the 
 //Items in the ListView
 HashMap<String , Object> temp;

 //total number of rows in the ListView
 int noOfPlayers=names.length;

 //now populate the ArrayList players
 for(int i=0;i<noOfPlayers;i++)
 {
  temp=new HashMap<String, Object>();

  temp.put("name", names[i]);
  temp.put("team", teams[i]);    
  temp.put("photo", photos[i]);

  //add the row to the ArrayList
  originalValues.add(temp);        
 }
 //searchResults=OriginalValues initially
 searchResults=new ArrayList<HashMap<String,Object>>(originalValues);

 //create the adapter
 //first param-the context
 //second param-the id of the layout file 
 //you will be using to fill a row
 //third param-the set of values that
 //will populate the ListView
 final CustomAdapter adapter=new CustomAdapter(this, R.layout.players_layout,searchResults); 

 //finally,set the adapter to the default ListView
 playersListView.setAdapter(adapter);
 searchBox.addTextChangedListener(new TextWatcher() {

 public void onTextChanged(CharSequence s, int start, int before, int count) {
   //get the text in the EditText
   String searchString=searchBox.getText().toString();
   int textLength=searchString.length();

          //clear the initial data set
   searchResults.clear();

   for(int i=0;i<originalValues.size();i++)
   {
  String playerName=originalValues.get(i).get("name").toString();
  if(textLength<=playerName.length()){
  //compare the String in EditText with Names in the ArrayList
    if(searchString.equalsIgnoreCase(playerName.substring(0,textLength)))
    searchResults.add(originalValues.get(i));
  }
   }

   adapter.notifyDataSetChanged();
 }

 public void beforeTextChanged(CharSequence s, int start, int count,
     int after) {


   }

   public void afterTextChanged(Editable s) {


   }
  });
 }

As you can see,this is pretty much the previous post,except for this part:
searchBox.addTextChangedListener(new TextWatcher() {

 public void onTextChanged(CharSequence s, int start, int before, int count) {
   //get the text in the EditText
   String searchString=searchBox.getText().toString();
   int textLength=searchString.length();
  
          //clear the initial data set
          searchResults.clear();

   for(int i=0;i<originalValues.size();i++)
   {
  String playerName=originalValues.get(i).get("name").toString();
  if(textLength<=playerName.length()){
  //compare the String in EditText with Names in the ArrayList
    if(searchString.equalsIgnoreCase(playerName.substring(0,textLength)))
    searchResults.add(originalValues.get(i));
  }
   }

   adapter.notifyDataSetChanged();
 }

 public void beforeTextChanged(CharSequence s, int start, int count,
     int after) {


   }

   public void afterTextChanged(Editable s) {


   }
  });
Examine the code and you will find it pretty self-explanatory.searchBox is the EditText.What we have done here is to attach a TextWatcher interface to the searchBox.
The methods of this interface will be called when the text in the searchBox changes.You can have a look at the interface here.When the text in the searchBox changes,we clear the initial data set and compare the entered  text with all the names in the ArrayList of players.All names matching the search query are added to the ArrayList searchResult and notifyDataSetChanged() is called on the adapter.
notifyDataSetChanged() tells the CustomAdapter that the underlying data has changed and that the ListView should refresh itself.As the ArrayList searchResult is used for displaying the ListView,the changed data will be reflected in the ListView.That's it!! We''ve done it!! :).Have a look at the CustomAdapter if you have'nt already:
//define your custom adapter
 private class CustomAdapter extends ArrayAdapter<HashMap<String, Object>>
 {

  public CustomAdapter(Context context, int textViewResourceId,
    ArrayList<HashMap<String, Object>> Strings) {

   //let android do the initializing :)
   super(context, textViewResourceId, Strings);
  }


  //class for caching the views in a row  
  private class ViewHolder
  {
   ImageView photo;
   TextView name,team;

  }

  ViewHolder viewHolder;

  @Override
  public View getView(int position, View convertView, ViewGroup parent) {

   if(convertView==null)
   {
    convertView=inflater.inflate(R.layout.players_layout, null);
    viewHolder=new ViewHolder();

     //cache the views
     viewHolder.photo=(ImageView) convertView.findViewById(R.id.photo);
     viewHolder.name=(TextView) convertView.findViewById(R.id.name);
     viewHolder.team=(TextView) convertView.findViewById(R.id.team);

      //link the cached views to the convertview
      convertView.setTag(viewHolder);

   }
   else
    viewHolder=(ViewHolder) convertView.getTag();


   int photoId=(Integer) searchResults.get(position).get("photo");

   //set the data to be displayed
   viewHolder.photo.setImageDrawable(getResources().getDrawable(photoId));
   viewHolder.name.setText(searchResults.get(position).get("name").toString());
   viewHolder.team.setText(searchResults.get(position).get("team").toString());

   //return the view to be displayed
   return convertView;
  }

 }

And here's everything put together:
public class SearchCustomListViewActivity extends ListActivity {

  //ArrayList thats going to hold the search results
  ArrayList<HashMap<String, Object>> searchResults;

  //ArrayList that will hold the original Data
  ArrayList<HashMap<String, Object>> originalValues;
  LayoutInflater inflater;

  @Override
  public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 
 setContentView(R.layout.main);
 final EditText searchBox=(EditText) findViewById(R.id.searchBox);
 ListView playerListView=(ListView) findViewById(android.R.id.list);

 //get the LayoutInflater for inflating the customomView
 //this will be used in the custom adapter
 inflater=(LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);

 //these arrays are just the data that 
 //I'll be using to populate the ArrayList
 //You can use our own methods to get the data
 String names[]={"Ronaldo","Messi","Torres","Iniesta",
    "Drogba","Gerrard","Rooney","Xavi"}; 

 String teams[]={"Real Madrid","Barcelona","Chelsea",
    "Barcelona","Chelsea","Liverpool",
    "ManU","Barcelona"};
 Integer[] photos={R.drawable.cr7,R.drawable.messi,
    R.drawable.torres,R.drawable.iniesta,
    R.drawable.drogba,R.drawable.gerrard,
    R.drawable.rooney,R.drawable.xavi};

 originalValues=new ArrayList<HashMap<String,Object>>();

 //temporary HashMap for populating the 
 //Items in the ListView
 HashMap<String , Object> temp;

 //total number of rows in the ListView
 int noOfPlayers=names.length;

 //now populate the ArrayList players
 for(int i=0;i<noOfPlayers;i++)
 {
  temp=new HashMap<String, Object>();

  temp.put("name", names[i]);
  temp.put("team", teams[i]);    
  temp.put("photo", photos[i]);

  //add the row to the ArrayList
  originalValues.add(temp);        
 }
 //searchResults=OriginalValues initially
 searchResults=new ArrayList<HashMap<String,Object>>(originalValues);

 //create the adapter
 //first param-the context
 //second param-the id of the layout file 
 //you will be using to fill a row
 //third param-the set of values that
 //will populate the ListView
 final CustomAdapter adapter=new CustomAdapter(this, R.layout.players_layout,searchResults); 

 //finally,set the adapter to the default ListView
 cocktailListView.setAdapter(adapter);
 searchBox.addTextChangedListener(new TextWatcher() {

 public void onTextChanged(CharSequence s, int start, int before, int count) {
   //get the text in the EditText
   String searchString=searchBox.getText().toString();
   int textLength=searchString.length();
   searchResults.clear();

   for(int i=0;i<originalValues.size();i++)
   {
  String playerName=originalValues.get(i).get("name").toString();
  if(textLength<=playerName.length()){
  //compare the String in EditText with Names in the ArrayList
    if(searchString.equalsIgnoreCase(playerName.substring(0,textLength)))
    searchResults.add(originalValues.get(i));
  }
   }

   adapter.notifyDataSetChanged();
 }

 public void beforeTextChanged(CharSequence s, int start, int count,
     int after) {


   }

   public void afterTextChanged(Editable s) {


   }
  });
 }


 //define your custom adapter
 private class CustomAdapter extends ArrayAdapter<HashMap<String, Object>>
 {

  public CustomAdapter(Context context, int textViewResourceId,
    ArrayList<HashMap<String, Object>> Strings) {

   //let android do the initializing :)
   super(context, textViewResourceId, Strings);
  }


  //class for caching the views in a row  
  private class ViewHolder
  {
   ImageView photo;
   TextView name,team;

  }

  ViewHolder viewHolder;

  @Override
  public View getView(int position, View convertView, ViewGroup parent) {

   if(convertView==null)
   {
    convertView=inflater.inflate(R.layout.players_layout, null);
    viewHolder=new ViewHolder();

     //cache the views
     viewHolder.photo=(ImageView) convertView.findViewById(R.id.photo);
     viewHolder.name=(TextView) convertView.findViewById(R.id.name);
     viewHolder.team=(TextView) convertView.findViewById(R.id.team);

      //link the cached views to the convertview
      convertView.setTag(viewHolder);

   }
   else
    viewHolder=(ViewHolder) convertView.getTag();


   int photoId=(Integer) searchResults.get(position).get("photo");

   //set the data to be displayed
   viewHolder.photo.setImageDrawable(getResources().getDrawable(photoId));
   viewHolder.name.setText(searchResults.get(position).get("name").toString());
   viewHolder.team.setText(searchResults.get(position).get("team").toString());

   //return the view to be displayed
   return convertView;
  }

 }
}