Sunday 13 May 2012

Solving the "bitmap size exceeds VM budget error"

java.lang.OutOfMemoryError: bitmap size exceeds VM budget.
This is one phrase you'll come across frequently in android while you try to load several large bitmaps in your program.And Why?Because the heap size available to an application is limited and large bitmaps are the easiest way of exhausting it.
So how do you work around it?
Understand that mostly,it is pointless to load a bitmap in its full capacity onto a device's screen that fits on your palm.So,all you have to do is to load a scaled down version of the bitmap and everything will be well and good.But how do you scale down a bitmap for loading? Configure and use the BitmapFactory.Options class while decoding the bitmap .
Here,for simplicity,I'll be loading a bitmap on to a single ImageView.

In simple words,here's what they mean:
1. BitmapFactory.Options.inJustDecodeBounds: When set to true,we will be able to measure the bitmap dimensions,without actually loading the bitmap into memory.
2.BitmapFactory.Options.inSampleSize: The value of this int allows us to load a scaled down version of the bitmap into memory. As an example,if the value set to this variable is 2, the resulting bitmap will have a width half the original width and height set to half the original height.
That is,
        New width= original width/2
        New Height= original height/2.

Here,we'll be dedicating a class for loading the bitmap and in the class here's what we are going to do:
1. Measure the actual height and width of the bitmap by setting inJustDecodeBounds=true;
2. Obtain an appropriate scale(which we will set to inSampleSize),using the actual height and width.
3. Load the scaled down bitmap using the inSampleSize.


Note that this article is an implementation of the guidelines given here.


As I have said,my main.xml contains only a single ImageView,for the purpose of demonstration.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <ImageView 
        android:id="@+id/bitmapView"
        android:layout_width="100dp"
        android:layout_height="100dp"/>

</LinearLayout>



Now for the code,read the comments and you will find it self explanatory.


package com.blog.simplebitmaploader;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Environment;
import android.widget.ImageView;

public class BitmapLoaderActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        //path of the file to be loaded
        String filePath=Environment.getExternalStorageDirectory()+
          "/Sunset_Beach.jpg";
  
        setContentView(R.layout.main);
        ImageView bitmapView=(ImageView) findViewById(R.id.bitmapView);
        
        //load the scaled down version
        bitmapView.setImageBitmap(BitmapLoader.loadBitmap(filePath, 100, 100));
    }
}

class BitmapLoader
{
  public static int getScale(int originalWidth,int originalHeight,
       final int requiredWidth,final int requiredHeight)
  {
 //a scale of 1 means the original dimensions 
 //of the image are maintained
 int scale=1;
       
 //calculate scale only if the height or width of 
 //the image exceeds the required value.
 if((originalWidth>requiredWidth) || (originalHeight>requiredHeight)) 
   {
       //calculate scale with respect to
    //the smaller dimension
    if(originalWidth<originalHeight)
   scale=Math.round((float)originalWidth/requiredWidth);
    else
   scale=Math.round((float)originalHeight/requiredHeight);

    }
      
  return scale;
  }
 
  public static BitmapFactory.Options getOptions(String filePath,
    int requiredWidth,int requiredHeight)
   {
  
 BitmapFactory.Options options=new BitmapFactory.Options();
 //setting inJustDecodeBounds to true
 //ensures that we are able to measure
 //the dimensions of the image,without
 //actually allocating it memory
 options.inJustDecodeBounds=true;
  
 //decode the file for measurement
 BitmapFactory.decodeFile(filePath,options);
  
 //obtain the inSampleSize for loading a 
 //scaled down version of the image.
 //options.outWidth and options.outHeight 
 //are the measured dimensions of the 
 //original image
 options.inSampleSize=getScale(options.outWidth,
   options.outHeight, requiredWidth, requiredHeight);
  
 //set inJustDecodeBounds to false again
 //so that we can now actually allocate the
 //bitmap some memory
 options.inJustDecodeBounds=false;
  
 return options;
  
  }
 
 
 
 public static Bitmap loadBitmap(String filePath,
   int requiredWidth,int requiredHeight){
 

  BitmapFactory.Options options= getOptions(filePath,
     requiredWidth, requiredHeight);
  
  return BitmapFactory.decodeFile(filePath,options);
 }
}



Simple,right?? The only disadvantage of this code is that everything related to the bitmap is being done in the UI thread,which could slow down your application.In my next post,we'll see how this code can be executed in a different thread.For now,adios..:):):).



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;
  }

 }
}


Sunday 25 March 2012

Custom ListView in Android

Ever wondered how to implement a ListView that displays content that YOU want??  If  the answer is yes,then READ ON!!! :)

Here,we'll be building a ListView that displays an ImageView and two TextViews.Something like this:

Each row will show an image,the name of a player and his team.

First step: Create the layout file that will display a row in the ListView.Mine is players.xml and here it 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">
    
    <ImageView 
        android:id="@+id/photo"
        android:layout_width="55dp"
        android:layout_height="70dp"
        android:layout_alignParentLeft="true"
        android:scaleType="fitXY"/>
    
    <TextView 
        android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@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>
Now that you have the layout,lets get on to the coding part.
Create your Acivity by extending ListActivity
public class CustomListActivity extends ListActivity {
 //some code goes here
}
The important thing to note here is that ListActivity HAS AN IMPLICIT LAYOUT associated
with it.This default layout contains a ListView that fills the screen.You are not required to define any other layout for the ListActivity,although its perfectly fine to do so.:).

I've used the onCreate method of my ListActivity to define the data that will be displayed in the ListView and to set the adapter.
public class CustomListViewActivity extends ListActivity {
 
 //the ArrayList that will hold the data to be displayed in the ListView 
 ArrayList<HashMap<String, Object>> players;
 LayoutInflater inflater;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        //not necessary as ListActivity has an 
        //implicitly defined Layout(with a ListView of course)
        //setContentView(R.layout.main);   
        
        //get the LayoutInflater for inflating the customomView
        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>>();
       
       //HashMap for storing a single row
       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
       CustomAdapter adapter=new CustomAdapter(this, R.layout.players_layout,players); 
   
       //finally,set the adapter to the default ListView
       setListAdapter(adapter);
    }
This is whats been done here:
1. Set the names,teams and the drawables for each row as arrays.
2. Add all of these to an ArrayList players.
   Hence,now we have all the data to be displayed in the ArrayList players sound and safe.

Now comes the important part: Creating your CustomAdapter
The adapter is the heart of the ListView.It provides the ListView with the data and the Views that will be displayed in the list.
The default implementation of the ArrayAdapter provides only  a simple TextView as a list item.However,its fairly simple to create your own adapter to replace them with custom layouts.
All you have to do is define a class which extends the ArrayAdapter class and Override its getView() method. The ListView will then set the View returned by getView() as a list item.Note that getView() will be called every time a list item is displayed.
This is how its done:


//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)
   {        
                                //inflate the custom layout                        
    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) 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());
  
   //return the view to be displayed
  return convertView;
  }
      
    }

I'll explain what just happened here.
The getView() method is called everytime android needs to display a list item.
We may inflate the layout and return it everytime;but android provides a brilliant yet simple mechanism for re-using the inflated Views.
The convertView parameter passed to getView() is the View recycling mechanism of android.
Android only creates the Views that are visible to the user.Sometimes,maybe one or two extra.
As the user scrolls,the View of the list item that leaves the screen can be re-used to display the list item that enters the screen.
And this recycled View is given to the getView() method as a parameter.
Thus,we require to inflate only when convertView is null.

That being said,how do you effectively re-use the convertView?? That's where the ViewHolder class comes into play.
Every time  convertView is null,the layout is inflated,and an instance of the ViewHolder class is created to hold the inflated views.This instance is then tagged to the convertView.
If  getView()  gives a non- null convertView,then simply get the already tagged ViewHolder from it and use it.

Thus effectively,in a list with a 1000 items,we can bring down the layout inflation from 1000 times to maybe 5 or 6 times!!! Imagine the performance enhancement this technique will give,given that inflating and parsing the View hierarchy is a costly affair.