【译】原文: https://www.bignerdranch.com/blog/customizing-android-listview-rows-subclassing/
每个Android开发者都会碰到自定义ListView中item布局,并填充数据的情况,而你首先想到的应该就是ViewHolder模式吧。但是ViewHolder模式使用起来太过于死板,实际上我们可以做得更好。在本文中,我们会探索另外一种可供选择的方式:使用RelativeLayout的子类来封装完成定制化的工作。
目标
为了说明目的,我们首先创建了一个典型自定义的ListView,它的每一行包括一个ImageView和两个排列在它旁边的TextView,每个item的父布局为RelativeLayout,如下图所示。你可以在Github上查看它的代码。
当为ListView中的item创建自定义view的时候,我们有下面的需求:
- 使用自定义布局管理子控件的排列方式
- 当滚动时,重复利用已创建的view
- 高效识别子视图,并为其填充数据
ViewHolder模式所带来的问题
我们在网上看到的一种实现都大同小异,这就是ViewHolder模式。简而言之,它需要下面的步骤:
- 为ListView中item创建一个布局
- 创建ViewHolder类,存储item中子控件
- 对于ListView中的每个item,创建一个ViewHolder的实例,并通过findViewById()与ViewHolder中成员绑定
- 使用setTag(),将item与ViewHolder绑定
- 对重复使用的item重用ViewHolder对象
因为下面的原因,我并不喜欢这个模式:
- 它把太多的职责放在了Adapter的getView()方法中
- ViewHolder类过于公式化
- 每个视图需要根据tag强转为合适的ViewHolder类型
- ViewHolder类需要知道list中item内部细节,违反了封装的特点
因此,我决定提供一种可供选择的方式——子类化,而不是一直抱怨。
使用子类实现自定义
我会使用RelativeLayout的子类命名为ItemView作为我们是定义布局的根视图,而不是创建一个一般的视图——ViewHolder类与Adapter类的实现,它们了解太多内部实现的细节。Item是呈现数据的模型对象,它里面有三个属性:图片的url,标题以及描述。
1 | public class Item { |
ItemView有责任将Item中数据展示在屏幕上。作为ItemView的使用者(在Adapter的子类中),我想我的职责越简单越好,我真的只需要做好下面两件事:
- 为ListView的每一个item创建或者重用ItemView
- 将当前行的数据Item与ItemView关联起来
我们可以在ItemAdapter类中查看相关的API:
1 | public class ItemAdapter extends ArrayAdapter<Item> { |
这里有两行代码很有意思,首先,如果我们没有可重用的view,那么就调用ItemView.inflate(ViewGroup)
这个静态方法获取ItemView的实例。其次,我们使用setItem(Item)
方法为当前ItemView提供要展示的数据。所有这些细节都封装在ItemVIew内部。
ItemView通过成员变量存储对子视图的引用,把自己作为自己的ViewHolder。
1 | public class ItemView extends RelativeLayout { |
inflate(ViewGroup)
这个静态方法使创建并正确配置ItemView变得非常简单,同时使用XML文件完成自定义布局的配置。
1 | public static ItemView inflate(ViewGroup parent) { |
它使用参数中的parent
来获取context
,然后将布局文件映射为视图并返回。布局文件如下所示:
1 | <com.bignerdranch.android.listitemviewdemo.ItemView |
该布局文件只包含一个ItemView,在ItemView中声明了边距。要特别注意的是,它并没有子节点,那么我们需要的ImageView以及两个TexeView在哪呢?实际上,在ItemView的构造方法中,我们完成了对子视图的创建。
1 | public ItemView(Context context, AttributeSet attrs, int defStyle) { |
该构造方法最终会在inflate包含ItemView视图的时候调用,它首先会调用父类的构造方法,然后infalte ItemView中的子视图。子视图的布局文件如下:
1 | <?xml version="1.0" encoding="utf-8"?> |
根节点为merge
标签,它表明在inflation的过程中,merge
标签下的所有子控件都会被添加到parent
参数中,并传递到构造方法中的inflate(...)
方法。之后我们调用setupChildren()
方法,通过findViewById(int)
完成查找控件的工作,这样就可以把控件与成员变量关联起来了。
1 | private void setupChildren() { |
到此,ItemView就已经实现了传统的VIewHolder的功能,能通过自己的成员变量将子视图的引用缓存起来。
为了让大家信服,我同时提供了setItem(Item)
方法让调用者把Item与每个子视图绑定起来。
1 | public void setItem(Item item) { |
这就是我们实现了一种新的模式。尽管我们需要创建多创建一个布局文件,并且为了inflation多实现了一个方法,但是优点是显而易见的:
Adapter
类的实现被大大的简化了- 能够很容易的通过xml文件或者代码创建
ItemView
- 任何扩展都能够在ItemView以及布局文件内部来完成
- 不再需要创建多余的ViewHolder类
Subclassing works
下一次想到ViewHolder模式的时候,尝试用一下本文提到的新的模式。该模式良好的封装很容易让你充分自定义ItemView而不再需要考虑那些令人烦扰的细节。在视图重用的过程中,我敢说它跟ViewHolder模式一样好。