2012年11月20日火曜日

HowTo: ListView, Adapter, getView and different list items’ layouts in one ListView

ListView and Adapter Basics

How it works:

  1. ListView asks adapter "give me a view" (getView) for each item of the list
  2. A new View is returned and displayed

Next question – what if we have one billion items? Create new view for each item? The answer is NO:-) Android caches views for you.

There's a component in Android called "Recycler". I drawed a picture based on Romain Guypresentation at Google IO '09.

  1. If you have 1 billion items – there are only visible items in the memory + view in recycler.
  2. ListView asks for a view type1 first time (getView) x visible items. convertView is null in getView – you create new view of type1 and return it.
  3. ListView asks for a view type1 when one item1 is outside of the window and new item the same type is comming from the bottom. convertView is not null = item1. You should just set new data and return convertView back. No need to create view again.

Let's write a simple code and put System.out to the getView:

public class MultipleItemsList extends ListActivity {         private MyCustomAdapter mAdapter;         @Override      public void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          mAdapter = new MyCustomAdapter();          for (int i = 0; i < 50; i++) {              mAdapter.addItem("item " + i);          }          setListAdapter(mAdapter);      }         private class MyCustomAdapter extends BaseAdapter {             private ArrayList mData = new ArrayList();          private LayoutInflater mInflater;             public MyCustomAdapter() {              mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);          }             public void addItem(final String item) {              mData.add(item);              notifyDataSetChanged();          }             @Override          public int getCount() {              return mData.size();          }             @Override          public String getItem(int position) {              return mData.get(position);          }             @Override          public long getItemId(int position) {              return position;          }             @Override          public View getView(int position, View convertView, ViewGroup parent) {              System.out.println("getView " + position + " " + convertView);              ViewHolder holder = null;              if (convertView == null) {                  convertView = mInflater.inflate(R.layout.item1, null);                  holder = new ViewHolder();                  holder.textView = (TextView)convertView.findViewById(R.id.text);                  convertView.setTag(holder);              } else {                  holder = (ViewHolder)convertView.getTag();              }              holder.textView.setText(mData.get(position));              return convertView;          }         }         public static class ViewHolder {          public TextView textView;      }  }

Run the program and see what happens:

getView was called 9 times. convertView is null for all visible items

02-05 13:47:32.559: INFO/System.out(947): getView 0 null  02-05 13:47:32.570: INFO/System.out(947): getView 1 null  02-05 13:47:32.589: INFO/System.out(947): getView 2 null  02-05 13:47:32.599: INFO/System.out(947): getView 3 null  02-05 13:47:32.619: INFO/System.out(947): getView 4 null  02-05 13:47:32.629: INFO/System.out(947): getView 5 null  02-05 13:47:32.708: INFO/System.out(947): getView 6 null  02-05 13:47:32.719: INFO/System.out(947): getView 7 null  02-05 13:47:32.729: INFO/System.out(947): getView 8 null

Then scroll the list slightly down (until item 10 appears):

convertView is still null because there is still no view in the recycler (border of item1 is still visible at the top:))

02-05 13:48:25.169: INFO/System.out(947): getView 9 null

Let's scroll list a little more:

Bingo! convertView is not null: item1 goes off the screen directly to the Recycler and item11 is created based on item1.

02-05 13:48:42.879: INFO/System.out(947): getView 10 android.widget.LinearLayout@437430f8

scroll more just to check what hapens:

02-05 14:01:31.069: INFO/System.out(947): getView 11 android.widget.LinearLayout@437447d0  02-05 14:01:31.142: INFO/System.out(947): getView 12 android.widget.LinearLayout@43744ff8  02-05 14:01:31.279: INFO/System.out(947): getView 13 android.widget.LinearLayout@43743fa8  02-05 14:01:31.350: INFO/System.out(947): getView 14 android.widget.LinearLayout@43745820  02-05 14:01:31.429: INFO/System.out(947): getView 15 android.widget.LinearLayout@43746048  02-05 14:01:31.550: INFO/System.out(947): getView 16 android.widget.LinearLayout@43746870  02-05 14:01:31.669: INFO/System.out(947): getView 17 android.widget.LinearLayout@43747098  02-05 14:01:31.839: INFO/System.out(947): getView 18 android.widget.LinearLayout@437478c0  02-05 14:03:30.900: INFO/System.out(947): getView 19 android.widget.LinearLayout@43748df0  02-05 14:03:32.069: INFO/System.out(947): getView 20 android.widget.LinearLayout@437430f8

convertView is not null as we expected. After item11 goes off the screen, it view (@437430f8) comes as convertView for item 21. simple.

Different list items' layouts

Let's move to the "more complicated" example. How about to add separator somewhere to the list.

You should do the following:

  1. Override getViewTypeCount() – return how many different view layouts you have
  2. Override getItemViewType(int) – return correct view type id by position
  3. Create correct convertView (depending on view item type) in getView

Simple, isn't it? Code snippet:

public class MultipleItemsList extends ListActivity {         private MyCustomAdapter mAdapter;         @Override      public void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          mAdapter = new MyCustomAdapter();          for (int i = 1; i < 50; i++) {              mAdapter.addItem("item " + i);              if (i % 4 == 0) {                  mAdapter.addSeparatorItem("separator " + i);              }          }          setListAdapter(mAdapter);      }         private class MyCustomAdapter extends BaseAdapter {             private static final int TYPE_ITEM = 0;          private static final int TYPE_SEPARATOR = 1;          private static final int TYPE_MAX_COUNT = TYPE_SEPARATOR + 1;             private ArrayList mData = new ArrayList();          private LayoutInflater mInflater;             private TreeSet mSeparatorsSet = new TreeSet();             public MyCustomAdapter() {              mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);          }             public void addItem(final String item) {              mData.add(item);              notifyDataSetChanged();          }             public void addSeparatorItem(final String item) {              mData.add(item);              // save separator position              mSeparatorsSet.add(mData.size() - 1);              notifyDataSetChanged();          }             @Override          public int getItemViewType(int position) {              return mSeparatorsSet.contains(position) ? TYPE_SEPARATOR : TYPE_ITEM;          }             @Override          public int getViewTypeCount() {              return TYPE_MAX_COUNT;          }             @Override          public int getCount() {              return mData.size();          }             @Override          public String getItem(int position) {              return mData.get(position);          }             @Override          public long getItemId(int position) {              return position;          }             @Override          public View getView(int position, View convertView, ViewGroup parent) {              ViewHolder holder = null;              int type = getItemViewType(position);              System.out.println("getView " + position + " " + convertView + " type = " + type);              if (convertView == null) {                  holder = new ViewHolder();                  switch (type) {                      case TYPE_ITEM:                          convertView = mInflater.inflate(R.layout.item1, null);                          holder.textView = (TextView)convertView.findViewById(R.id.text);                          break;                      case TYPE_SEPARATOR:                          convertView = mInflater.inflate(R.layout.item2, null);                          holder.textView = (TextView)convertView.findViewById(R.id.textSeparator);                          break;                  }                  convertView.setTag(holder);              } else {                  holder = (ViewHolder)convertView.getTag();              }              holder.textView.setText(mData.get(position));              return convertView;          }         }         public static class ViewHolder {          public TextView textView;      }  }

Let's run what we wrote. Yo will see separators after each 4-th item in the list.

In the log – nothing exceptional – all convertView is null for visible items both types.

02-05 15:19:03.080: INFO/System.out(1035): getView 0 null type = 0  02-05 15:19:03.112: INFO/System.out(1035): getView 1 null type = 0  02-05 15:19:03.130: INFO/System.out(1035): getView 2 null type = 0  02-05 15:19:03.141: INFO/System.out(1035): getView 3 null type = 0  02-05 15:19:03.160: INFO/System.out(1035): getView 4 null type = 1  02-05 15:19:03.170: INFO/System.out(1035): getView 5 null type = 0  02-05 15:19:03.180: INFO/System.out(1035): getView 6 null type = 0  02-05 15:19:03.190: INFO/System.out(1035): getView 7 null type = 0  02-05 15:19:03.210: INFO/System.out(1035): getView 8 null type = 0  02-05 15:19:03.210: INFO/System.out(1035): getView 9 null type = 1

Scroll list and see what happens:

02-05 15:19:54.160: INFO/System.out(1035): getView 10 null type = 0  02-05 15:19:57.440: INFO/System.out(1035): getView 11 android.widget.LinearLayout@43744528 type = 0  02-05 15:20:01.310: INFO/System.out(1035): getView 12 android.widget.LinearLayout@43744eb0 type = 0  02-05 15:20:01.880: INFO/System.out(1035): getView 13 android.widget.LinearLayout@437456d8 type = 0  02-05 15:20:02.869: INFO/System.out(1035): getView 14 null type = 1  02-05 15:20:06.489: INFO/System.out(1035): getView 15 android.widget.LinearLayout@43745f00 type = 0  02-05 15:20:07.749: INFO/System.out(1035): getView 16 android.widget.LinearLayout@43747170 type = 0  02-05 15:20:10.250: INFO/System.out(1035): getView 17 android.widget.LinearLayout@43747998 type = 0  02-05 15:20:11.661: INFO/System.out(1035): getView 18 android.widget.LinearLayout@437481c0 type = 0  02-05 15:20:13.180: INFO/System.out(1035): getView 19 android.widget.LinearLayout@437468a0 type = 1  02-05 15:20:16.900: INFO/System.out(1035): getView 20 android.widget.LinearLayout@437489e8 type = 0  02-05 15:20:25.690: INFO/System.out(1035): getView 21 android.widget.LinearLayout@4374a8d8 type = 0

convertView is null for separator view type until first separator is visible. When it goes off the screen – view also comes to the Recycler and convertView comes to play.

MultipleItemsList.zip – source code

0 件のコメント:

コメントを投稿