ListView 和 RecyclerView
最常用和最难用的控件
由于手机屏幕空间有限,无法显示全部内容。当有大量数据需要展示的时候,借助列表控件。通过手指上下滑动,使得屏幕内外的数据不断进出。
最基本的列表工作模式需要列表控件、数据源,列表控件能够进行交互和展示数据。但是列表控件不与数据源直接打交道,Adapter 接口充当桥梁,关联数据源与列表控件,增强可扩展性,适配不同数据类型数据源。例如:ArrayAdapter 数组、CursorAdapter 游标。
数据源可能来自:
- 静态数据
- 网络数据
- 数据库
ListView
ListView extends AdapterView extends ViewGroup.
- Adapter 管理数据源
- AdapterView 展示数据并处理交互
数据无法直接传递给 ListView,需要借助 setAdapter()
适配器来完成。例如 ArrayAdapter<>
泛型指定要适配的数据类型。
ListView listView = (ListView) findViewById(R.id.listview);
listView.setAdapter(adapter);
自定义适配器
适配数据源并重写一组父类方法:
构造函数:例如 ArrayAdapter 依次传入当前上下文、ListView 子项布局 id、数据源。
ArrayAdapter(Context context, int resource, int textViewResourceId, List<T> objects)
getView() 方法:用于每个子项(单行)进入屏幕可视区域时候调用,根据数据源绘制子项布局。
程序示例:
public class MySimpleArrayAdapter extends ArrayAdapter<String> {
private final Context context;
private final String[] values;
public MySimpleArrayAdapter(Context context, String[] values) {
super(context, R.layout.rowlayout, values);
this.context = context;
this.values = values;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View rowView = inflater.inflate(R.layout.rowlayout, parent, false);
TextView textView = (TextView) rowView.findViewById(R.id.label);
ImageView imageView = (ImageView) rowView.findViewById(R.id.icon);
textView.setText(values[position]);
// Change the icon for Windows and iPhone
String s = values[position];
if (s.startsWith("Windows7") || s.startsWith("iPhone")
|| s.startsWith("Solaris")) {
imageView.setImageResource(R.drawable.no);
} else {
imageView.setImageResource(R.drawable.ok);
}
return rowView;
}
}
其他常用方法:
getCount() 方法
返回适配器表示的数据源中一共有多少项数据。
notifyDataSetChanged() 方法
数据源的数据发生变化,通知 ListView 更新数据重新绘制视图。
提升 ListView 运行效率
避免在 Adapter 的 getView() 方法中重新加载布局(子项布局)
public abstract View getView(int position, View convertView, ViewGroup parent)
convertView 用于将加载好的布局进行缓存,根据 convertView 是否为空,判断能否重用布局,减少 LayoutInflater.inflate()
调用次数从而提升性能。
减少 findViewById() 方法获取控件实例的调用次数
通过内部类 ViewHolder 对控件实例进行缓存,调用 View 的 setTag()
方法,将 ViewHolder 对象存储在 View 中。
程序示例:
public class MyPerformanceArrayAdapter extends ArrayAdapter<String> {
private final Activity context;
private final String[] names;
static class ViewHolder {
public TextView text;
public ImageView image;
}
public MyPerformanceArrayAdapter(Activity context, String[] names) {
super(context, R.layout.rowlayout, names);
this.context = context;
this.names = names;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View rowView = convertView;
// reuse views
if (rowView == null) {
LayoutInflater inflater = context.getLayoutInflater();
rowView = inflater.inflate(R.layout.rowlayout, null);
// configure view holder
ViewHolder viewHolder = new ViewHolder();
viewHolder.text = (TextView) rowView.findViewById(R.id.TextView01);
viewHolder.image = (ImageView) rowView
.findViewById(R.id.ImageView01);
rowView.setTag(viewHolder);
}
// fill data
ViewHolder holder = (ViewHolder) rowView.getTag();
String s = names[position];
holder.text.setText(s);
if (s.startsWith("Windows7") || s.startsWith("iPhone")
|| s.startsWith("Solaris")) {
holder.image.setImageResource(R.drawable.no);
} else {
holder.image.setImageResource(R.drawable.ok);
}
return rowView;
}
}
存在多种类型的子项布局的场景
基本实现方式:
- 定义视图类型常量
- 重写
getViewTypeCount()
方法和getItemViewType(int position)
方法 - 重写
getView()
方法
getViewTypeCount() 方法
返回一共有多少个不同的视图类型(布局),这些视图将由 getView()
方法创建。
getItemViewType(int position) 方法
根据子项所处的位置判断具体类型并返回。
程序示例:
@Override
public int getViewTypeCount() {
return 2;
}
@Override
public int getItemViewType(int position) {
return (contactList.get(position).getContactType() == ContactType.CONTACT_WITH_IMAGE) ? 0 : 1;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
int type = getItemViewType(position);
if (v == null) {
// Inflate the layout according to the view type
LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (type == 0) {
// Inflate the layout with image
v = inflater.inflate(R.layout.image_contact_layout, parent, false);
}
else {
v = inflater.inflate(R.layout.simple_contact_layout, parent, false);
}
}
// fill data
Contact c = contactList.get(position);
TextView surname = (TextView) v.findViewById(R.id.surname);
TextView name = (TextView) v.findViewById(R.id.name);
TextView email = (TextView) v.findViewById(R.id.email);
if (type == 0) {
ImageView img = (ImageView) v.findViewById(R.id.img);
img.setImageResource(c.imageId);
}
surname.setText(c.surname);
name.setText(c.name);
email.setText(c.email);
return v;
}
ListView 的 RecycleBin 机制
ListView 即使加载成百上千条数据,依然不会发生 OOM 的原因——RecycleBin 机制。
RecycleBin 类中存在两个重要的数组:
- mActiveViews 屏幕上可见的 View
- mScrapViews 屏幕外不可见的 View
当 ListView 子项 View 进入屏幕可视区域时候,从 RecycleBin 的 mScrapViews 获取 View 作为 convertView 参数传递给 Adapter 的 getView()
方法。
ListView 有如 View 一般执行视图绘制流程 onMeasure()
、onLayout()
、onDraw()
。在 onLayout()
方法中会调用一个关键方法 layoutChildren()
,该方法由 ListView 具体实现进行子元素的布局,同时完成 ListView 对子项 View 的添加和删除操作。
layoutChildren()
方法主要逻辑:
- 若 Adapter 中的数据集发生变化,则将 ListView 中的所有子项 View 放到 RecycleBin 中的 mScrapViews 废弃 View 集合。
若 Adapter 中的数据集无变化,则将 ListView 中的所有子项 View 放到 RecycleBin 中的 mActiveViews 激活 View 集合。 - 调用
detachAllViewsFromParent()
方法解除子项 View 与 ListView 之间的关联。 - 重新将子项 View 添加到 ListView 中。根据 mLayoutMode 判断如何进行添加,
fillDown()
方法将子 View 从指定的 position 自上而下填充 ListView,fillUp()
则相反自下而上进行填充。
RecyclerView
自定义适配器
适配器继承自 RecyclerView.Adapter<>
,并将泛型指定为内部类 Adapter.ViewHolder。
重写一组父类方法:
-
onCreateViewHolder()
加载子项布局(LayoutInflaterinflate()
),创建 ViewHolder 实例。 -
onBindViewHolder()
用于每个子项(单行)进入屏幕可视区域时候调用,根据数据源位置绘制子项布局。 -
getItemCount()
返回数据源的长度
程序示例:
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
private String[] mDataset;
// Provide a reference to the views for each data item
// Complex data items may need more than one view per item, and
// you provide access to all the views for a data item in a view holder
public static class MyViewHolder extends RecyclerView.ViewHolder {
// each data item is just a string in this case
public TextView mTextView;
public MyViewHolder(TextView v) {
super(v);
mTextView = v;
}
}
// Provide a suitable constructor (depends on the kind of dataset)
public MyAdapter(String[] myDataset) {
mDataset = myDataset;
}
// Create new views (invoked by the layout manager)
@Override
public MyAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
// create a new view
TextView v = (TextView) LayoutInflater.from(parent.getContext())
.inflate(R.layout.my_text_view, parent, false);
...
MyViewHolder vh = new MyViewHolder(v);
return vh;
}
// Replace the contents of a view (invoked by the layout manager)
@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
holder.mTextView.setText(mDataset[position]);
}
// Return the size of your dataset (invoked by the layout manager)
@Override
public int getItemCount() {
return mDataset.length;
}
}
RecyclerView vs ListView
固有的 ViewHolder 模式规范
RecyclerView.Adapter 默认采用 ViewHolder 模式,减少 findViewById()
方法获取控件实例的调用次数。
使用 LayoutManager 支持多种布局方式
RecyclerView 借助 LayoutManager 能够灵活地将列表控件放入不同的容器(LinearLayout, GridLayout)。
ListView 布局只能实现纵向排列,而 RecyclerView 将排列工作 setLayoutManager()
交给 LayoutManager 布局排列接口,因此可以定制出不同排列方式(横向、瀑布流布局)。
通知 Adapter 的数据变化更加灵活
不仅 notifyDataSetChange()
方法,RecyclerView 可以使用 notifyItemRangeChanged()
等方法实现局部更新数据并重绘视图。
子项视图的动画效果更容易实现
- RecyclerView.ItemAnimator
- RecyclerView.ItemDecoration
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。