Friday, March 25, 2011

Complete Custom List Example

Listviews are a very useful part of Android applications.  However, they are much more useful when you create a custom listview.  With a custom listview, you can extend all the standard classes to very easily show more complex information in a simple way.  In creating my application, I combined several bits of code from many different places from the web to get my listview to act the way I wanted.  I thought there might be a few people out there that might find this stuff helpful so I figured I'd share a simple example I came up with.

My example will show a list of countries.  If you click on them, it will tell you the capitol.  If you long click, it will give a few simple options.  This is obviously not that useful and overly complicated but it provides a good framework for more complex implementations.

First, I created a custom Countries class to hold each object.
1:  public class Country {  
2:       public String name;  
3:       public String capitol;  
4:  }  
Code Block 1

Well that was pretty simple.  Next, create your activity and some global variables.
1:  public class CustomListSuperDemo extends Activity {  
2:       private final String[] countryList = {  
3:            "Afghanistan:Kabul",  
4:            ...  
5:            "United States:Washington D.C.",  
6:            ...  
7:            "Zimbabwe:Harare"  
8:       };  
9:       private static List<Country> myCountries = new LinkedList<Country>();  
10:       private MovieAdapter myAdapter;  
11:       private int selectedMovieIndex;  
12:       private static final int MENU_UPP = 0;  
13:       private static final int MENU_LOW = 1;  
Code Block 2

The countryList will be used later to create all the Country objects, which are held in the myCountries list.  You can get the full list from the full code.  Everything else are used for saving information when the user clicks a list item.  Next I define the behavior or the listview adapter.
1:       private static class MovieAdapter extends BaseAdapter {  
2:            private final LayoutInflater mInflater;  
3:            public MovieAdapter(Context context) {  
4:                 mInflater = LayoutInflater.from(context);  
5:            }  
6:            public int getCount() {  
7:                 return myCountries.size();  
8:            }  
9:            public Object getItem(int position) {  
10:                 return position;  
11:            }  
12:            public long getItemId(int position) {  
13:                 return position;  
14:            }  
15:            public View getView(int position, View convertView, ViewGroup parent) {  
16:                 ViewHolder holder;  
17:                 if (convertView == null) {  
18:                      convertView = mInflater.inflate(R.layout.listitem, null);  
19:                      holder = new ViewHolder();  
20:                 } else {  
21:                      holder = (ViewHolder) convertView.getTag();  
22:                 }  
23:                 holder.name = (TextView) convertView  
24:                      .findViewById(R.id.name);  
25:                 convertView.setTag(holder);  
26:                 holder.name.setText( myCountries.get(position).name );  
27:                 return convertView;  
28:            }  
29:            static class ViewHolder {  
30:                 TextView name;  
31:            }  
32:       }  
Code Block 3

Most of that is pretty simple but the getView function is where it gets fun.  This function is used to map your list of objects to individual views (what the user sees).  You can move lines 23-24 into the if statement (after line 19).  Be careful with this though.  I had an issue when doing this with an imageView because these are cached differently.  You can randomly get images show up in the wrong list item.  I just like my stuff like I show above just to be on the safe side.

Next I set up the UI and load the list.
1:       public void onCreate(Bundle savedInstanceState){  
2:            super.onCreate(savedInstanceState);  
3:            setContentView(R.layout.main);  
4:            new loadList().execute();  
5:    }  
6:       class loadList extends AsyncTask<Void, Movie, Void> {  
7:            ProgressDialog dialog;  
8:            protected void onPreExecute(){  
9:                 dialog = ProgressDialog.show(CustomListSuperDemo.this, "",  
10:                           "Loading. Please wait...", true);  
11:            }  
12:            protected Void doInBackground(Void... unused) {  
13:                 Arrays.sort( countryList );  
14:                 for( int i=0; i< countryList.length; ++i){  
15:                      //add it to the country list  
16:                      Country newCountry = new Country();  
17:                      newCountry.name = countryList[i].split(":")[0];  
18:                      newCountry.capitol = countryList[i].split(":")[1];  
19:                      myCountries.add(newCountry);  
20:                 }  
21:                 return(null);  
22:            }  
23:            protected void onPostExecute(Void unused){  
24:                 if( myCountries.size() == 0 ){  
25:                      ((TextView)findViewById(R.id.empty)).setVisibility(View.VISIBLE);  
26:                 }else{  
27:                      ((TextView)findViewById(R.id.empty)).setVisibility(View.GONE);  
28:                      ListView lv = (ListView) findViewById(R.id.movieList);  
29:                      lv.setAdapter(new MovieAdapter(CustomListSuperDemo.this));  
30:                      lv.setDivider(new ColorDrawable(Color.DKGRAY));  
31:                      lv.setDividerHeight(2);  
32:                      lv.setOnItemClickListener(new AdapterView.OnItemClickListener(){  
33:                           public void onItemClick(AdapterView<?> av, View v, int pos, long id){  
34:                                Toast.makeText(CustomListSuperDemo.this,  
35:                                     myCountries.get(pos).capitol + " is my capitol.", Toast.LENGTH_SHORT).show();  
36:                           }  
37:                      });  
38:                      lv.setOnCreateContextMenuListener(new OnCreateContextMenuListener(){  
39:                           public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo){  
40:                             AdapterView.AdapterContextMenuInfo info =  
41:                               (AdapterView.AdapterContextMenuInfo) menuInfo;  
42:                             selectedMovieIndex = info.position;  
43:                             menu.setHeaderTitle("Options");  
44:                             menu.add(0, MENU_UPP, 0, "To Upper");  
45:                             menu.add(0, MENU_LOW, 0, "To Lower");  
46:                           }  
47:                      });  
48:                      myAdapter = (MovieAdapter)lv.getAdapter();  
49:                 }  
50:                 try{  
51:                      dialog.dismiss();  
52:                 }catch(Exception e){}  
53:            }  
54:       }  
Code Block 4

In onCreate, I load the UI and call an instance of AsyncTask to load the data.  This is useful when object data takes a while to load (load from database or download from web).  Once this is done, I set up the display and add the click events.  This is where I use the two MENU constants.  When the user longClicks, they are used to create the buttons.  These are also use one more time below.  I also save a reference to myAdapter and the selected position to make things easier later on.

Only one bit of code left: processing the long click.
1:       public boolean onContextItemSelected(MenuItem item) {  
2:            new processClick().execute(item.getItemId());  
3:            return true;  
4:       }  
5:       class processClick extends AsyncTask<Integer, Void, Void> {  
6:            ProgressDialog dialog;  
7:            protected void onPreExecute(){  
8:                 dialog = ProgressDialog.show(CustomListSuperDemo.this, "",  
9:                           "Loading. Please wait...", true);  
10:            }  
11:            protected Void doInBackground(Integer... tmpbtnID){  
12:                 try {  
13:                      Thread.sleep(1000);  
14:                 } catch (InterruptedException e){  
15:                      e.printStackTrace();  
16:                 }  
17:                 switch(tmpbtnID[0]){  
18:                 case MENU_UPP:  
19:                      myCountries.get(selectedMovieIndex).name =  
20:                           myCountries.get(selectedMovieIndex).name.toUpperCase();  
21:                      break;  
22:                 case MENU_LOW:  
23:                      myCountries.get(selectedMovieIndex).name =  
24:                           myCountries.get(selectedMovieIndex).name.toLowerCase();  
25:                      break;  
26:                 }  
27:                 return(null);  
28:            }  
29:            protected void onPostExecute(Void unused){  
30:                 myAdapter.notifyDataSetChanged();  
31:                 dialog.dismiss();  
32:            }  
33:       }  
Code Block 5

I process the click in another AsyncTask because some actions may take a while.  Once I'm all done, I notify the saved adapter that it should update.

If anyone needs something explained a little more, just let me know.  You can get the full code here: Custom ListView Example.zip

No comments:

Post a Comment