从 ListView 到 RecyclerView 的用法浅析

文章目录
  1. 1. ListView 的用法
  2. 2. RecyclerView 的用法
  3. 3. ListView 与 RecyclerView 的简单对比
  4. 4. 参考文献

要走好明天的路,必须记住昨天走过的路,思索今天正在走着的路。

ListView,一种在垂直滚动列表中显示条目的视图;RecyclerView,一种在局限的窗口呈现大数据集合的灵活视图。RecyclerView 部件是 ListView 的一种更高级且更灵活的版本。

以上描述来自官网。

移动设备屏幕空间有限,导致在屏幕上一次性显示的内容也是有限的。当需要显示大量的数据时,设想有这样的控件,可以帮助用户只通过手指上下滑动,就可以让屏幕外的数据滚动到屏幕内,同时,屏幕上原有的数据会滚动出屏幕。如此,便可以优雅地解决在局限的屏幕上显示大量数据的问题。ListView 和 RecyclerView 便适用于此。

不过,自 Android 5.0 推出以来,RecyclerView 在很多地方都在逐步取代 ListView,这也是官方推崇的。“江山代有才人出,各领风骚数百年”。如今,已要来到 Android 7.0 的时代,RecyclerView 的使用也很普遍了,或许,ListView 正逐步从 Android 的大舞台退出,RecyclerView 即将独领风骚。

下面浅析从 ListView 到 RecyclerView 的用法。

ListView 的用法

ListViewDemo 地址: ListViewDemo,学习总结自郭霖的《第一行代码》。

新建一个 ListViewDemo 项目,默认持续下一步直至完成。然后,删掉 activity_main.xml 中默认建好的 TextView,建立一个 ListView 如下:

1
2
3
4
5
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent">

</ListView>

接着,新建一个实体类 Fruit,作为 ListView 适配器的适配类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Fruit {

private String name;
private int imageId;

public Fruit(String name, int imageId) {
this.name = name;
this.imageId = imageId;
}

public String getName() {
return name;
}

public int getImageId() {
return imageId;
}
}

name 是水果的名字,imageId 是水果对应图片的资源 id。然后,为 ListView 的子项自定义一个布局,在 layout 目录下新建 fruit_item.xml 如下:

1
2
3
4
5
6
7
8
9
10
11
<ImageView
android:id="@+id/fruit_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>


<TextView
android:id="@+id/fruit_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="10dp"/>

ImageView 用于显示水果的图片,TextView 用于显示水果的名称。

由于数据无法直接传递给 ListView,需要借助适配器完成。Android 中提供了很多适配器的实现类,这里使用 ArrayAdapter,通过泛型指定适配的数据类型,再在构造函数中传入适配的数据。自定义一个适配器 FruitAdapter,继承自 ArrayAdapter,泛型指定为 Fruit 类。

同时,优化 ListView 的使用效率:

一. FruitAdapter 中的 getView() 方法每次都将布局加载一遍,这样,快速滚动的话会影响性能。故利用好 convertView 参数,缓存之前加载好的布局,再重用。

二. 使用 ViewHolder 模式,其充分利用 ListView 的视图缓存机制,避免每次调用 getView() 时通过 findViewById() 实例化控件。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class FruitAdapter extends ArrayAdapter<Fruit> {

private int resourceId;

public FruitAdapter(Context context, int textViewResourceId, List<Fruit> objects) {
super(context, textViewResourceId, objects);
resourceId = textViewResourceId;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
Fruit fruit = getItem(position);
View view;
ViewHolder viewHolder;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, null);
viewHolder = new ViewHolder();
viewHolder.fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
viewHolder.fruitName = (TextView) view.findViewById(R.id.fruit_name);
view.setTag(viewHolder); //将 ViewHolder 存储在 View 中
} else {
view = convertView;
viewHolder = (ViewHolder) view.getTag(); //重新获取 ViewHolder, 找到缓存的布局.
}
viewHolder.fruitImage.setImageResource(fruit.getImageId());
viewHolder.fruitName.setText(fruit.getName());
return view;
}

class ViewHolder {
ImageView fruitImage;
TextView fruitName;
}
}

FruitAdapter 重写父类的构造函数,将上下文、ListView 子项布局的 id 和数据传递进来。同时,重写了 getView() 方法,每个子项滚动进屏幕内时调用该方法。其中,getItem() 方法获取当前项的 Fruit 实例。

增加判断与一个内部类 ViewHolder,利用视图缓存机制进行缓存。

若 convertView 为空,则创建一个 ViewHolder 对象,将控件的实例存放在 ViewHolder 里。然后,调用 View 的 setTag() 方法,将 ViewHolder 对象存储在 View 中。若 convertView 不为空,则调用 View 的 getTag() 方法,重新取出 ViewHolder。如此,所有控件的实例都缓存在 ViewHolder 里。

最后,修改并完善 MainActivity 中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class MainActivity extends AppCompatActivity {

private List<Fruit> fruitList = new ArrayList<Fruit>();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits();

FruitAdapter adapter = new FruitAdapter(MainActivity.this, R.layout.fruit_item, fruitList);
ListView listView = (ListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
}

private void initFruits() {
Fruit apple = new Fruit("Apple", R.mipmap.ic_launcher);
fruitList.add(apple);
Fruit banana = new Fruit("Banana", R.mipmap.ic_launcher);

fruitList.add(banana);
Fruit orange = new Fruit("Orange", R.mipmap.ic_launcher);

fruitList.add(orange);
Fruit watermelon = new Fruit("Watermelon", R.mipmap.ic_launcher);

fruitList.add(watermelon);
Fruit pear = new Fruit("Pear", R.mipmap.ic_launcher);

fruitList.add(pear);
Fruit grape = new Fruit("Grape", R.mipmap.ic_launcher);

fruitList.add(grape);
Fruit pineapple = new Fruit("Pineapple", R.mipmap.ic_launcher);

fruitList.add(pineapple);
Fruit strawberry = new Fruit("Strawberry", R.mipmap.ic_launcher);

fruitList.add(strawberry);
Fruit cherry = new Fruit("Cherry", R.mipmap.ic_launcher);

fruitList.add(cherry);
Fruit mango = new Fruit("Mango", R.mipmap.ic_launcher);

fruitList.add(mango);
Fruit peach = new Fruit("Peach", R.mipmap.ic_launcher);

fruitList.add(peach);
Fruit lemon = new Fruit("Lemon", R.mipmap.ic_launcher);

fruitList.add(lemon);
Fruit pitaya = new Fruit("Pitaya", R.mipmap.ic_launcher);

fruitList.add(pitaya);
Fruit durin = new Fruit("Durin", R.mipmap.ic_launcher);

fruitList.add(durin);
}


}

添加了 initFruits() 方法,初始化所有的水果数据。将水果的名字和对应的图片 id 传入 Fruit 类的构造函数中,再把创建好的对象添加到水果列表中。而后,在 onCreate() 方法里创建了 FruitAdapter 对象,将 FruitAdapter 作为适配器传递给 ListView。

至此,ListView 的简单综合应用浅析完毕,运行程序,如下:

更多如动态修改 ListView、使 ListView 具有弹性、自动显示与隐藏布局 ListView、模仿微信聊天布局 ListView、动态改变点击 Item 布局的 ListView 等,详见 GitHub

RecyclerView 的用法

RecyclerViewDemo 地址: RecyclerViewDemo,学习总结自徐宜生的《 Android 群英传》。

首先,在 gradle 中引入compile 'com.android.support:recyclerview-v7:23.4.0'的依赖。由于 RecyclerView 封装好了 ViewHolder,再实现其功能。新建一个 RecyclerViewDemo 如上所示,同时添加了 Spinner 和按钮:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<android.support.v7.widget.RecyclerView
android:id="@+id/rc_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="9"
android:scrollbars="vertical"/>


<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">


<Spinner
android:id="@+id/spinner"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:entries="@array/recycler_list"/>


<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="addRecycler"
android:text="Add"/>


<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="delRecycler"
android:text="Delete"/>


</LinearLayout>

在 strings.xml 中新建 Spinner 中用到的 array:

1
2
3
4
<string-array name="recycler_list">
<item>LinearLayoutManager</item>
<item>GridLayoutManager</item>
</string-array>

与 ListView 的用法一样,使用一个合适的数据适配器来加载数据,自定义一个适配器 RecyclerAdapter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.ViewHolder> {

private List<String> mData;
public OnItemClickListener mOnItemClickListener;

public RecyclerAdapter(List<String> data) {
this.mData = data;
}

public void setOnItemClickListener(OnItemClickListener itemClickListener) {
this.mOnItemClickListener = itemClickListener;
}

public interface OnItemClickListener {
void onItemClick(View view, int position);
}

public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

public TextView mTextView;

public ViewHolder(View itemView) {
super(itemView);
mTextView = (TextView) itemView;
mTextView.setOnClickListener(this);
}

// 通过接口回调来实现 RecyclerView 的点击事件
@Override
public void onClick(View view) {
if (mOnItemClickListener != null) {
mOnItemClickListener.onItemClick(view, getPosition());
}
}
}

@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
// 将布局转化为 View ,并传递给 RecyclerView 封装好的 ViewHolder
View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.rc_item,
viewGroup, false);
return new ViewHolder(view);
}

@Override
public void onBindViewHolder(ViewHolder viewHolder, int i) {
//建立起 ViewHolder 中视图与数据的关联
viewHolder.mTextView.setText(mData.get(i) + i);
}

@Override
public int getItemCount() {
return mData.size();
}

}

一个典型的 RecyclerView,通过 onCreateViewHolder 将 List item 的布局转化为 View,并传递给 RecyclerView 封装好的 ViewHolder,就可以将数据与视图关联起来。但是要注意的是,Android 并没有给 RecyclerView 增进点击事件,需要自己使用接口回调机制,创建一个点击事件的接口,如上面的:

1
2
3
4
5
6
7
public void setOnItemClickListener(OnItemClickListener itemClickListener) {
this.mOnItemClickListener = itemClickListener;
}

public interface OnItemClickListener {
void onItemClick(View view, int position);
}

然后,建立一个类似于 ListView 的 List Item 视图 rc_item.xml:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:textSize="30sp"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="3dp"
android:background="#bebebe"
android:orientation="vertical">

</TextView>

另外,自定义 LayoutManager 可以方便地创建不同的布局,为较深入 RecyclerView 的使用,本篇同时创建了线性布局和网格布局。MainActivity 中的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
public class MainActivity extends AppCompatActivity {

private RecyclerView mRecyclerView;
private RecyclerAdapter mRecyclerAdapter;
private RecyclerView.LayoutManager mLayoutManager;

private Spinner mSpinner;

private List<String> mData = new ArrayList<String>();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mRecyclerView = (RecyclerView) findViewById(R.id.rc_list);
mLayoutManager = new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.setHasFixedSize(true);
//设置显示动画
mRecyclerView.setItemAnimator(new DefaultItemAnimator());

mSpinner = (Spinner) findViewById(R.id.spinner);
mSpinner.setOnItemSelectedListener(
new AdapterView.OnItemSelectedListener() {

@Override
public void onItemSelected(AdapterView<?> parent,
View view, int position,
long id)
{

if (position == 0) {
mRecyclerView.setLayoutManager(
// 设置为线性布局
new LinearLayoutManager(MainActivity.this));
} else if (position == 1) {
mRecyclerView.setLayoutManager(
// 设置为表格布局
new GridLayoutManager(MainActivity.this, 3));
} else if (position == 2) {
}
}

@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});

//增加测试数据
mData.add("Recycler");
mData.add("Recycler");
mData.add("Recycler");

mRecyclerAdapter = new RecyclerAdapter(mData);
mRecyclerView.setAdapter(mRecyclerAdapter);

mRecyclerAdapter.setOnItemClickListener(
new RecyclerAdapter.OnItemClickListener() {

@Override
public void onItemClick(final View view, int position) {
// 设置点击动画
view.animate()
.translationZ(15f).setDuration(300)
.setListener(new AnimatorListenerAdapter() {

@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
view.animate()
.translationZ(1f)
.setDuration(500).start();
}
}).start();
}
});
}

public void addRecycler(View view) {
mData.add("Recycler");
int position = mData.size();
if (position > 0) {
mRecyclerAdapter.notifyDataSetChanged();
}
}

public void delRecycler(View view) {
int position = mData.size();
if (position > 0) {
mData.remove(position - 1);
mRecyclerAdapter.notifyDataSetChanged();
}
}

}

RecyclerView 为条目位置提供了布局管理器,同时,为条目设置了操作动画。代码中也添加了动态修改功能,点击按钮,便可以增加或删除条目,notifyDataSetChanged() 方法帮助实现刷新布局的功能。

最后,结合使用 Spinner,程序运行结果分别如下:

纵向布局:

横向布局:

ListView 与 RecyclerView 的简单对比

初看起来,ListView 与 RecyclerView 好像并没有什么不同。实际上,对比来看,RecyclerView 相对于 ListView,主要包含这几处新的特性,如 ViewHolder,ItemDecorator,LayoutManager,SmoothScroller 以及增加或删除 item 时 item 的动画等。具体如下:

ViewHolder

ViewHolder 是用来保存视图引用的类。

ListView 中,ViewHolder 得自定义,是一种推荐的提高性能的方式,不过不是必须的。不使用 ViewHolder 的话,ListView 每次 getView() 的时候都会调用 findViewById(),影响性能。

RecyclerView 中,使用 RecyclerView.ViewHolder 是必须的,不过实现起来稍微复杂些,提高了性能。BaseAdapter 使用 RecyclerView.ViewHolder,绑定 position 到上面。

LayoutManager

ListView 只能在垂直方向上滚动,API 没有提供在水平方向上滚动的支持。

RecyclerView 在滚动上的功能扩展很多,支持多种类型列表的展示需求,如支持水平和垂直方向上滚动列表的 LinearLayoutManager;支持交叉网格风格列表的 StaggeredLayoutManager,类似于瀑布流;支持网格展示,可以水平或垂直滚动的 GridLayoutManager,如展示图片的画廊。

ItemAnimator

ListView 中,删除或添加 item 时,是无法产生动画效果的。

RecyclerView 中,RecyclerView.ItemAnimator 用于添加、删除或移动 item 时显示动画效果。若不自定义 ItemDecorator,可以使用 DefaultItemAnimator。

Adapter

ListView 的 Adapter 中,getView() 将视图和 position 绑定起来,也能通过 registerDataObserver 在 Adapter 中注册一个观察者。ListView 有三个 Adapter 的默认实现,分别是 ArrayAdapter,CursorAdapter 和 SimpleCursorAdapter。

RecyclerView 中,RecyclerView.AdapterDataObserver 是这个观察者,并且,其 Adapter 拥有除内置的内 DB 游标和 ArrayList 之外的所有功能。

ItemDecoration

ListView 中,要在 item 之间添加间隔符,在布局文件中对 ListView 添加如下属性:

1
2
android:divider="@android:color/transparent"
android:dividerHeight="5dp"

RecyclerView 默认情况下并没在 item 之间展示间隔符,想要添加间隔符,须使用 RecyclerView.ItemDecoration 类来实现。

OnItemTouchListener

ListView 通过 AdapterView.OnItemClickListener 接口探测点击事件。

RecyclerView 通过 RecyclerView.OnItemTouchListener 接口探测触摸事件,给予拦截触摸事件更多的控制权限。

本人才疏学浅,如有疏漏错误之处,望读者中有识之士不吝赐教,谢谢。

1
Email: [email protected] / WeChat: Wolverine623

您也可以关注我个人的微信公众号码农六哥第一时间获得博客的更新通知,或后台留言与我交流

参考文献

1.第一行代码

2.Android 群英传