首页 > 代码库 > ListView显示不同行以及数据重用

ListView显示不同行以及数据重用

Handling ListViews with Multiple Row Types

When you start writing Android Apps it isn’t long before you need to use ListViews. ListViews are easy to get started with, but it’s also very easy to write inefficient lists that wreak havoc on scrolling performance. There are lot of things you can do to improve scrolling performance. You should always use convertViews to reduce creating new views. You should use the ViewHolder pattern outlined in theAndroid API example to reduce lookup time in layouts. If you have images in your lists the same principles outlined in my quora answer for iOS UITableViewsapplies to Android ListViews.

But what happens when not every item in your list view looks the same? This is often the case when you want to build a ListView that’s presenting some type of feed where some rows have images, some have videos, and some are just text and they come in no particular order. You can and should still use all the methods mentioned above. But item view reuse suddenly becomes much more complicated. You definitely don’t want to go back to not reusing the convertView, just to handle multiple row types. This is where the view type methods in the Adapter come in.

Before moving on let’s do a quick review of how ListViews and their adapters interact. If you’ve used ListViews and Adapters before before you can probably skip this paragraph. When you set an adapter on a ListView by using the setAdapater(Adapter) method, you are telling the ListView to use the adapter to tell it what to show in the list. The two key methods for the Adapter are getCountand getView. The list view calls the adapter’s getCount to know how many items exist in the list. Then only for the rows that are visible on the screen it calls getView which returns the view to show at that item.

The two additional methods android Adapters provide for managing different row types are: getItemViewType(int position) and getViewTypeCount(). The list view uses these methods create different pools of views to reuse for different types of rows.

Pools? ConvertViews? Huh?

Let’s step back for a minute and take look at how the whole convertView business works. Internally ListViews try to do a lot of work to scroll smoothly. One of the things they do is to reuse view instances when scrolling. Everytime a ListView gets a view through the getView method in your adapter, the ListView keeps that view in a pool of views that can be reused. The convertView parameter in getView is a view from this pool. If you get a non-null convertView in the getView method of your adapter, that means the ListView is telling you: “Sup dawg, I heard you want to show something here. Instead of wasting time by creating a new view, just take the one I am giving you and change its contents.”

So what about those type methods?

getViewTypeCount() tells the ListView how many of these view pools to keep. And getItemViewType(int position) tells the ListView which of these pools the view at this position belongs to. That way the ListView can give you just the right type of view as the convertView for reuse in later getView calls.

A good way to think about these pools is that ListView keeps an array of pools. The getViewTypeCount is the size of this pool array, and getItemViewType gives the index of the pool to use. What this means is that it’s important to return 0 indexed numbers in getItemViewType. It’s not a tag, it’s an index into an array.

It may help to think of a negative case to understand how this works. If you return the wrong index in getItemViewType for a particular row, then the ListView will obligingly pass you the wrong view in a convertView when you scroll. Your reuse code will then look for something that doesn’t exist and KABLAMO! Your app will crash. So it’s important to be careful about passing the right view type.

That’s a lot of words yo. Gimme some code. Show me how it really works.

Ok. Let’s use an example to explain. Let’s say we are making an app which helps you learn about animals. Let’s call this app… aah… Animals. On its home screen the app shows you an eclectic collection of Animals. Ideally you want to show an image and the name, but sometimes you don’t have an image for the animal. In that case you want to show a completely different layout in your row. Instead of the image you want to give a short description of the animal.

Animals App Screenshot

Our core model for this class is a POJO called Animal which has three fields: imageId, name, and description. The adapter is given a list of these animals to display. Since we have two types of rows our getViewTypeCount method for the adapter simply returns 2.

public int getViewTypeCount() {    return 2;}The getItemViewType method returns the right index based on the data:public int getItemViewType(int position) {    //we have an image so we are using an ImageRow    if (animals.get(position).getImageId() != null) return 0;    //we don‘t have an image so we are using a Description Row    else return 1;}

Our getView method uses the same logic in getItemViewType and branch to a different way to fill up the view:

public View getView(int position, View convertView, ViewGroup parent) {    //first get the animal from our data model    Animal animal = animals.get(position);    //if we have an image so we setup an the view for an image row    if (animal.getImageId() != null) {        ImageRowViewHolder holder;        View view;        //don‘t have a convert view so we‘re going to have to create a new one        if (convertView == null) {            ViewGroup viewGroup =(ViewGroup)LayoutInflater.from(AnimalHome.this)                    .inflate(R.layout.image_row, null);            //using the ViewHolder pattern to reduce lookups            holder = newImageRowViewHolder((ImageView)viewGroup.findViewById(R.id.image),                        (TextView)viewGroup.findViewById(R.id.title));            viewGroup.setTag(holder);            view = viewGroup;        }        //we have a convertView so we‘re just going to use it‘s content        else {            //get the holder so we can set the image            holder = (ImageRowViewHolder)convertView.getTag();            view = convertView;        }        //actually set the contents based on our animal        holder.imageView.setImageResource(animal.getImageId());        holder.titleView.setText(animal.getName());        return view;    }    //basically the same as above but for a layout with title and description    else {        DescriptionRowViewHolder holder;        View view;        if (convertView == null) {            ViewGroup viewGroup =(ViewGroup)LayoutInflater.from(AnimalHome.this)                    .inflate(R.layout.text_row, null);            holder = newDescriptionRowViewHolder((TextView)viewGroup.findViewById(R.id.title),                    (TextView)viewGroup.findViewById(R.id.description));            viewGroup.setTag(holder);            view = viewGroup;        } else {            view = convertView;            holder = (DescriptionRowViewHolder)convertView.getTag();        }        holder.descriptionView.setText(animal.getDescription());        holder.titleView.setText(animal.getName());        return view;    }}

 

That’s all there is to it. You now know how to use the view type methods to handle different row layouts in your lists. Class is over.

Umm wait… That’s some ugly lookin’ code. Can’t we do better?

Glad you asked! There are three things particularly ugly about this code. First we are using magic numbers for the values returned by our getItemViewType and getViewTypeCount methods. Second, we are repeating the same branching pattern in two different methods, getView and getItemViewType. Third, that getView method is long. All these things together make this code brittle and hard to maintain over the long term.

So how do we deal with all these problems? We introduce the concept of a Row object. You can think of a Row as a controller for each item in your list. It’s an interface that is implemented by the two different types of Rows in our example: ImageRow and DescriptionRow. When we construct our adapter we take the list of animals it’s given and create the right Row object for each animal.

AnimalAdapter(List<Animal> animals) {    rows = new ArrayList<Row>();//member variable    for (Animal animal : animals) {        //if it has an image, use an ImageRow        if (animal.getImageId() != null) {            rows.add(new ImageRow(LayoutInflater.from(AnimalHome.this), animal));        } else {//otherwise use a DescriptionRow            rows.add(new DescriptionRow(LayoutInflater.from(AnimalHome.this), animal));        }    }}

 

So what do these Row objects actually do? Well let’s take a look at the interface definition:

public interface Row {    public View getView(View convertView);    public int getViewType();}

 

This probably looks very familiar. That’s because these methods look almost exactly like getView and getItemViewType methods from the Adapter interface we talked about earlier. In each of these methods of the adapter we hand off the work to relevant method in the Row object itself. So when you call getView it gets the Row object for that position and asks it to return the correct view. For an ImageRow it returns a row where you have a title and an image, and for a DescriptionRow it returns a row that has a title and a description. Here’s what the getView and the getItemViewType methods on the adapter look like:

public int getItemViewType(int position) {    return rows.get(position).getViewType();}public View getView(int position, View convertView, ViewGroup parent) {    return rows.get(position).getView(convertView);}So what do view type methods actually return? We could just return 0 for ImageRows and 1 for DescriptionRows and when the the adapter’s getViewTypeCount method is called, return 2 and call it a day. But we wanted to avoid using magic numbers in our code so instead we use an Enum.public enum RowType {    IMAGE_ROW,    DESCRIPTION_ROW}

 

 

So getViewType for ImageRow returns RowType.IMAGE_ROW.ordinal(), and for DescriptionRow it returns RowType.DESCRIPTION_ROW.ordinal(). getViewTypeCount on our adapter simply returns RowType.values().length.

All in all our adapter looks like this:

private class AnimalAdapter extends BaseAdapter {    final List<Row> rows;    AnimalAdapter(List<Animal> animals) {        rows = new ArrayList<Row>();//member variable        for (Animal animal : animals) {            //if it has an image, use an ImageRow            if (animal.getImageId() != null) {                rows.add(new ImageRow(LayoutInflater.from(AnimalHome.this), animal));            } else {//otherwise use a DescriptionRow                rows.add(new DescriptionRow(LayoutInflater.from(AnimalHome.this), animal));            }        }    }    @Override    public int getViewTypeCount() {        return RowType.values().length;    }    @Override    public int getItemViewType(int position) {        return rows.get(position).getViewType();    }    public int getCount() {        return rows.size();    }    public Object getItem(int position) {        return position;    }    public long getItemId(int position) {        return position;    }    public View getView(int position, View convertView, ViewGroup parent) {        return rows.get(position).getView(convertView);    }}

 

As you can see our Adapter code is super simple! It’s because it passed all the hard work to the Row objects which have clear ownership of what their views look like and how they behave. Obviously you’re itching to see the code for the whole app and how it all works together. So you can download the whole Animals app here.

If you want to really understand how this pattern works do the following exercise with the downloaded code. Add a third type of row: ImageDescriptionRow. If you have all three pieces of data, image, title and description for an animal, then show the title and image just like the ImageRow but also show a description below spanning the width of the row.

摘抄自:http://logc.at/2011/10/10/handling-listviews-with-multiple-row-types/