RecyclerView的应用

RecyclerView是Android 5.0以后提出的新UI控件,可以用来代替传统的ListView。但是RecyclerView并不会完全替代ListView,因为两者的使用场景不一样。但是RecyclerView的出现会让很多开源项目被废弃,例如横向滚动的ListView, 横向滚动的GridView, 瀑布流控件,因为RecyclerView能够实现所有这些功能,这是由于RecyclerView对各个功能进行解耦,从而相对于ListView有更好的拓展性。本篇文章着重讲述RecyclerView的使用方式方式上,以及和ListView的对比。

使用RecyclerView

具体使用是需要在代码中看对应的功能实现就好了。对于AndroidX项目来说,直接使用即可,无需引入依赖。

activity_main.xml:

 1<?xml version="1.0" encoding="utf-8"?>
 2<LinearLayout
 3    xmlns:android="http://schemas.android.com/apk/res/android"
 4    xmlns:app="http://schemas.android.com/apk/res-auto"
 5    xmlns:tools="http://schemas.android.com/tools"
 6    android:layout_width="match_parent"
 7    android:layout_height="match_parent"
 8    android:orientation="vertical"
 9    android:padding="10dp"
10    tools:context=".MainActivity">
11    <LinearLayout
12        android:orientation="horizontal"
13        android:layout_width="match_parent"
14        android:layout_height="wrap_content">
15        <Button
16            android:text="添加数据"
17            android:onClick="onClickAddData"
18            android:layout_width="0dp"
19            android:layout_weight="1"
20            android:layout_height="wrap_content"/>
21        <Button
22            android:text="横向排列"
23            android:onClick="onClickHorizontal"
24            android:layout_width="0dp"
25            android:layout_weight="1"
26            android:layout_marginStart="3dp"
27            android:layout_height="wrap_content"/>
28        <Button
29            android:text="反向展示"
30            android:onClick="onClickReverse"
31            android:layout_width="0dp"
32            android:layout_weight="1"
33            android:layout_marginStart="3dp"
34            android:layout_height="wrap_content"/>
35    </LinearLayout>
36
37    <LinearLayout
38        android:orientation="horizontal"
39        android:layout_width="match_parent"
40        android:layout_height="wrap_content">
41        <Button
42            android:id="@+id/btn_linear_layout"
43            android:text="线性布局"
44            android:onClick="onChangeLayout"
45            android:layout_width="0dp"
46            android:layout_weight="1"
47            android:layout_height="wrap_content"/>
48        <Button
49            android:id="@+id/btn_grid_layout"
50            android:text="网格布局"
51            android:onClick="onChangeLayout"
52            android:layout_width="0dp"
53            android:layout_weight="1"
54            android:layout_marginStart="3dp"
55            android:layout_height="wrap_content"/>
56        <Button
57            android:id="@+id/btn_staggered_grid_layout"
58            android:text="瀑布流布局"
59            android:onClick="onChangeLayout"
60            android:layout_width="0dp"
61            android:layout_weight="1"
62            android:layout_marginStart="3dp"
63            android:layout_height="wrap_content"/>
64    </LinearLayout>
65
66    <LinearLayout
67        android:orientation="horizontal"
68        android:layout_width="match_parent"
69        android:layout_height="wrap_content">
70        <Button
71            android:layout_width="0dp"
72            android:layout_weight="1"
73            android:layout_height="wrap_content"
74            android:text="插入一条数据"
75            android:onClick="onInsertDataClick"/>
76
77        <Button
78            android:layout_width="0dp"
79            android:layout_weight="1"
80            android:layout_height="wrap_content"
81            android:text="删除一条数据"
82            android:layout_marginStart="3dp"
83            android:onClick="onRemoveDataClick"/>
84    </LinearLayout>
85
86
87
88    <androidx.recyclerview.widget.RecyclerView
89        android:id="@+id/recycler_view"
90        android:layout_width="match_parent"
91        android:layout_height="wrap_content"/>
92
93</LinearLayout>

item.xml

 1<?xml version="1.0" encoding="utf-8"?>
 2<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 3    android:orientation="horizontal"
 4    android:background="#CDDC39"
 5    android:layout_margin="4dp"
 6    android:layout_width="match_parent"
 7    android:layout_height="wrap_content">
 8
 9    <ImageView
10        android:padding="5dp"
11        android:id="@+id/iv"
12        android:layout_width="50dp"
13        android:layout_height="50dp"
14        android:scaleType="fitXY"/>
15
16    <TextView
17        android:id="@+id/tv"
18        android:layout_width="match_parent"
19        android:layout_height="match_parent"
20        android:textColor="@color/black"
21        android:gravity="center_vertical"
22        android:layout_marginStart="8dp"/>
23</LinearLayout>

MyRecycleViewAdapter.java

  1/**
  2 * 1、继承RecycleView.Adapter
  3 * 2、绑定ViewHolder
  4 * 3、实现Adapter的相关方法
  5 */
  6public class MyRecycleViewAdapter extends RecyclerView.Adapter<MyRecycleViewAdapter.MyViewHolder> {
  7
  8    private final Context context;
  9    private final RecyclerView recyclerView;
 10    private List<String> dataSource;
 11    private OnItemClickListener listener;
 12
 13    public MyRecycleViewAdapter(Context context, RecyclerView recyclerView){
 14        this.context = context;
 15        this.recyclerView = recyclerView;
 16        this.dataSource = new ArrayList<>();
 17    }
 18
 19    public void setDataSource(List<String> dataSource) {
 20        this.dataSource = dataSource;
 21        notifyDataSetChanged();
 22    }
 23
 24    public void setListener(OnItemClickListener listener) {
 25        this.listener = listener;
 26    }
 27
 28    // 创建并返回ViewHolder
 29    @NonNull
 30    @Override
 31    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
 32        return new MyViewHolder(LayoutInflater.from(context).inflate(R.layout.item, parent, false));
 33    }
 34
 35    // 通过ViewHolder绑定数据
 36    @Override
 37    public void onBindViewHolder(@NonNull MyRecycleViewAdapter.MyViewHolder holder, int position) {
 38        holder.imageView.setImageResource(getIcon(position));
 39        holder.textView.setText(dataSource.get(position));
 40        LinearLayout.LayoutParams params;
 41        if(StaggeredGridLayoutManager.class.equals(recyclerView.getLayoutManager().getClass())){
 42            int randomHeight = getRandomHeight();
 43            // 只在瀑布流布局中使用随机高度
 44            params = new LinearLayout.LayoutParams(
 45                    ViewGroup.LayoutParams.MATCH_PARENT,
 46                    randomHeight < 50 ? dp2px(context, 50f): randomHeight
 47            );
 48        }else{
 49            params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
 50        }
 51        params.gravity = Gravity.CENTER;
 52        holder.textView.setLayoutParams(params);
 53
 54        holder.itemView.setOnClickListener(v -> listener.onItemClick(position));
 55    }
 56
 57    private int dp2px(Context context, float dpValue) {
 58        final float scale = context.getResources().getDisplayMetrics().density;
 59        return (int) (dpValue * scale + 0.5f);
 60    }
 61
 62    // 返回数据数量
 63    @Override
 64    public int getItemCount() {
 65        return dataSource.size();
 66    }
 67
 68    // 返回不同的随机ItemView高度
 69    private int getRandomHeight(){
 70        return (int)(Math.random() * 500);
 71    }
 72
 73    // 根据不同的position选择一个图片
 74    private int getIcon(int position){
 75        switch (position % 5){
 76            case 0:
 77                return R.drawable.ic_4k;
 78            case 1:
 79                return R.drawable.ic_5g;
 80            case 2:
 81                return R.drawable.ic_360;
 82            case 3:
 83                return R.drawable.ic_adb;
 84            case 4:
 85                return R.drawable.ic_alarm;
 86            default:
 87                return 0;
 88        }
 89    }
 90
 91    // 添加一条数据
 92    public void addData (int position) {
 93        dataSource.add(position, "插入的数据");
 94        notifyItemInserted(position);
 95        // 刷新ItemView
 96        notifyItemRangeChanged(position, dataSource.size() - position);
 97    }
 98
 99    // 删除一条数据
100    public void removeData (int position) {
101        dataSource.remove(position);
102        notifyItemRemoved(position);
103
104        // 刷新ItemView
105        notifyItemRangeChanged(position, dataSource.size() - position);
106    }
107
108    static class MyViewHolder extends RecyclerView.ViewHolder {
109        ImageView imageView;
110        TextView textView;
111        public MyViewHolder(@NonNull View itemView) {
112            super(itemView);
113            imageView = itemView.findViewById(R.id.iv);
114            textView = itemView.findViewById(R.id.tv);
115        }
116    }
117
118    interface OnItemClickListener {
119        void onItemClick(int position);
120    }
121}

MainActivity.java

 1public class MainActivity extends AppCompatActivity {
 2
 3    private RecyclerView recyclerView;
 4    private MyRecycleViewAdapter adapter;
 5    private LinearLayoutManager linearLayoutManager;
 6
 7    @Override
 8    protected void onCreate(Bundle savedInstanceState) {
 9        super.onCreate(savedInstanceState);
10        setContentView(R.layout.activity_main);
11        recyclerView = findViewById(R.id.recycler_view);
12        // 设置线性布局
13        linearLayoutManager = new LinearLayoutManager(this);
14        recyclerView.setLayoutManager(linearLayoutManager);
15
16        adapter = new MyRecycleViewAdapter(this, recyclerView);
17        adapter.setListener(position -> Toast.makeText(MainActivity.this, "第" + position + "数据被点击", Toast.LENGTH_SHORT).show());
18        recyclerView.setAdapter(adapter);
19
20    }
21
22    public void onClickAddData(View view) {
23        List<String> data = new ArrayList<>();
24        for (int i = 0; i < 30; i++) {
25            data.add("第" + i + "条数据");
26        }
27        adapter.setDataSource(data);
28    }
29
30    public void onClickHorizontal(View view) {
31        linearLayoutManager.setReverseLayout(false);
32        // 横向排列ItemView
33        linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
34        recyclerView.setLayoutManager(linearLayoutManager);
35    }
36
37
38    public void onClickReverse(View view) {
39        // 数据反向展示
40        linearLayoutManager.setReverseLayout(true);
41        // 数据纵向排列
42        linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
43        recyclerView.setLayoutManager(linearLayoutManager);
44    }
45
46    public void onChangeLayout(View view) {
47        switch (view.getId()){
48            case R.id.btn_linear_layout:
49                LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
50                recyclerView.setLayoutManager(linearLayoutManager);
51                break;
52            case R.id.btn_grid_layout:
53                GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 2);
54                recyclerView.setLayoutManager(gridLayoutManager);
55                break;
56            case R.id.btn_staggered_grid_layout:
57                StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
58                recyclerView.setLayoutManager(staggeredGridLayoutManager);
59                break;
60        }
61    }
62
63    // 插入一条数据
64    public void onInsertDataClick (View v) {
65        adapter.addData(1);
66    }
67
68    // 删除一条数据
69    public void onRemoveDataClick (View v) {
70        adapter.removeData(1);
71    }
72}
布局类 效果
LinearLayoutManager 以垂直或水平滚动列表方式显示项目
GridLayoutManager 在网格中显示项目
StaggeredGridLayoutManager 在分散对齐网格中显示项目

上述代码解析

RecyclerView使用步骤

1、创建Adapter:创建一个继承RecyclerView.Adapter<VH>的Adapter类(VH是ViewHolder的类名),记为MyRecycleViewAdapter。

2、创建ViewHolder:在MyRecycleViewAdapter中创建一个继承RecyclerView.ViewHolder的静态内部类,记为VH。ViewHolder的实现和ListView的ViewHolder实现几乎一样。

3、在MyRecycleViewAdapter中实现三个方法:

1// 映射ItemLayoutId,创建VH并返回
2onCreateViewHolder(ViewGroup parent, int viewType)
3
4// 为Holder设置指定数据
5onBindViewHolder(VH holder, int position)
6
7// 返回Item的个数
8getItemCount()

RecyclerView局部刷新

ListView通过adapter.notifyDataSetChanged()实现ListView的更新,这种更新方法的缺点是全局更新,即对每个Item View都进行重绘。但事实上很多时候,我们只是更新了其中一个Item的数据,其他Item其实可以不需要重绘。所以在上面的代码中:adapter.addData(1)adapter.removeData(1) 都是使用的局部刷新:

 1// 添加一条数据
 2public void addData (int position) {
 3  dataSource.add(position, "插入的数据");
 4
 5  notifyItemInserted(position);
 6  // 刷新ItemView
 7  notifyItemRangeChanged(position, dataSource.size() - position);
 8}
 9
10// 删除一条数据
11public void removeData (int position) {
12  dataSource.remove(position);
13  notifyItemRemoved(position);
14
15  // 刷新ItemView
16  notifyItemRangeChanged(position, dataSource.size() - position);
17}

如果是ListView要完成局部刷新就稍微复杂一点:

 1public void updateItemView(ListView listview, int position, Data data){
 2    int firstPos = listview.getFirstVisiblePosition();
 3    int lastPos = listview.getLastVisiblePosition();
 4
 5    // 可见才更新,不可见则在getView()时更新
 6    if(position >= firstPos && position <= lastPos){
 7        //listview.getChildAt(i)获得的是当前可见的第i个item的view
 8        View view = listview.getChildAt(position - firstPos);
 9        VH vh = (VH)view.getTag();
10        vh.text.setText(data.text);
11    }
12}

Item的点击/长按事件

 1interface OnItemClickListener {
 2    void onItemClick(int position);
 3}
 4
 5private OnItemClickListener listener;
 6
 7public void setListener(OnItemClickListener listener) {
 8    this.listener = listener;
 9}
10
11......
12
13public void onBindViewHolder(@NonNull MyRecycleViewAdapter.MyViewHolder holder, int position) {
14    ......
15    holder.itemView.setOnClickListener(v -> listener.onItemClick(position));
16    holder.itemView.setOnLongClickListener(v -> {
17        listener.onItemLongClick(position);
18        return false;
19    });
20}

其他说明

1、dp单位转px单位:

1private int dp2px(Context context, float dpValue) {
2    final float scale = context.getResources().getDisplayMetrics().density;
3    return (int) (dpValue * scale + 0.5f);
4}

2、ImageView的scaleType的属性 android:scaleType="center": 保持原图的大小,显示在ImageView的中心。当原图的size大于ImageView的size时,多出来的部分被截掉。

android:scaleType="center_inside": 以原图正常显示为目的,如果原图大小大于ImageView的size,就按照比例缩小原图的宽高,居中显示在ImageView中。如果原图size小于ImageView的size,则不做处理居中显示图片。

android:scaleType="center_crop": 以原图填满ImageView为目的,如果原图size大于ImageView的size,则与center_inside一样,按比例缩小,居中显示在ImageView上。如果原图size小于ImageView的size,则按比例拉升原图的宽和高,填充ImageView居中显示。

android:scaleType="matrix": 不改变原图的大小,从ImageView的左上角开始绘制,超出部分做剪切处理。

androd:scaleType="fit_xy": 把图片按照指定的大小在ImageView中显示,拉伸显示图片,不保持原比例,填满ImageView。

android:scaleType="fit_start": 把原图按照比例放大缩小到ImageView的高度,显示在ImageView的start(前部/上部)。

android:sacleType="fit_center": 把原图按照比例放大缩小到ImageView的高度,显示在ImageView的center(中部/居中显示)。

android:scaleType="fit_end": 把原图按照比例放大缩小到ImageView的高度,显示在ImageVIew的end(后部/尾部/底部)。

ListView和RecyclerView对比

ListView的一些优点: 1、可以通过addHeaderView(), addFooterView()添加头视图和尾视图。

2、可以通过"android:divider"设置自定义分割线。

3、通过setOnItemClickListener()和setOnItemLongClickListener()可以很方便的设置点击事件和长按事件。

这些功能在RecyclerView中都没有直接的接口,虽然实现起来很简单但还是要自己实现,所以ListView用来实现简单的显示功能更简单。

RecyclerView的优点: 1、默认已经实现了View的复用,回收机制更加完善。

2、默认支持局部刷新。

3、容易实现添加item、删除item的动画效果。

4、容易实现拖拽、侧滑删除等功能。

5、RecyclerView是一个插件式的实现,对各个功能进行解耦,从而扩展性比较好。

回收机制分析

ListView回收机制

ListView为了保证Item View的复用,实现了一套回收机制,该回收机制的实现类是RecycleBin,他实现了两级缓存:

View[] mActiveViews: 缓存屏幕上的View,在该缓存里的View不需要调用getView()。

ArrayList[] mScrapViews: 每个Item Type对应一个列表作为回收站,缓存由于滚动而消失的View,此处的View如果被复用,会以参数的形式传给getView()。 接下来我们通过源码分析ListView是如何与RecycleBin交互的。其实ListView和RecyclerView的layout过程大同小异,ListView的布局函数是layoutChildren(),实现如下:

 1void layoutChildren(){
 2    // 1. 如果数据被改变了,则将所有ItemView回收至scrapView  
 3    // 而RecyclerView会根据情况放入Scrap Heap或RecyclePool,否则回收至mActiveViews
 4    if(dataChanged) {
 5        for (int i = 0; i < childCount; i++) {
 6            recycleBin.addScrapView(getChildAt(i), firstPosition+i);
 7        }
 8    }else {
 9    	recycleBin.fillActiveViews(childCount, firstPosition);
10    }
11
12    // 2. 填充
13    switch(){
14        case LAYOUT_XXX:
15            fillXxx();
16            break;
17        case LAYOUT_XXX:
18            fillXxx();
19            break;
20    }
21
22    // 3. 回收多余的activeView
23    mRecycler.scrapActiveViews();
24}

其中fillXxx()实现了对Item View进行填充,该方法内部调用了makeAndAddView(),实现如下:

 1View makeAndAddView(){
 2    if(!mDataChanged) {
 3        child = mRecycler.getActiveView(position);
 4        if (child != null) {
 5            return child;
 6        }
 7    }
 8    child = obtainView(position, mIsScrap);
 9    return child;
10}

其中,getActiveView()是从mActiveViews中获取合适的View,如果获取到了,则直接返回,而不调用obtainView(),这也印证了如果从mActiveViews获取到了可复用的View,则不需要调用getView()。

obtainView()是从mScrapViews中获取合适的View,然后以参数形式传给了getView(),实现如下:

1View obtainView(int position){
2    final View scrapView = mRecycler.getScrapView(position);  // 从RecycleBin中获取复用的View
3    final View child = mAdapter.getView(position, scrapView, this);
4}

接下去我们介绍getScrapView(position)的实现,该方法通过position得到Item Type,然后根据Item Type从mScrapViews获取可复用的View,如果获取不到,则返回null,具体实现如下:

 1class RecycleBin{
 2    private View[] mActiveViews;           // 存储屏幕上的View
 3    private ArrayList<View>[] mScrapViews; // 每个item type对应一个ArrayList
 4    private int mViewTypeCount;            // item type的个数
 5    private ArrayList<View> mCurrentScrap; // mScrapViews[0]
 6    View getScrapView(int position) {
 7        final int whichScrap = mAdapter.getItemViewType(position);
 8        if(whichScrap < 0) {
 9            return null;
10        }
11        if(mViewTypeCount == 1) {
12             return retrieveFromScrap(mCurrentScrap, position);
13        }else if (whichScrap < mScrapViews.length) {
14            return retrieveFromScrap(mScrapViews[whichScrap], position);
15        }
16        return null;
17    }
18    private View retrieveFromScrap(ArrayList<View> scrapViews, int position){
19        int size = scrapViews.size();
20        if(size > 0){
21            return scrapView.remove(scrapViews.size() - 1);  // 从回收列表中取出最后一个元素复用
22        }else{
23            return null;
24        }
25    }
26}

RecyclerView回收机制

RecyclerView和ListView的回收机制非常相似,但是ListView是以View作为单位进行回收,RecyclerView是以ViewHolder作为单位进行回收。Recycler是RecyclerView回收机制的实现类,他实现了四级缓存:

mAttachedScrap: 缓存在屏幕上的ViewHolder。 mCachedViews: 缓存屏幕外的ViewHolder,默认为2个。ListView对于屏幕外的缓存都会调用getView()。 mViewCacheExtensions: 需要用户定制,默认不实现。 mRecyclerPool: 缓存池,多个RecyclerView共用。

主要需要关注的是 getViewForPosition()方法,因此此处介绍该方法的实现:

 1View getViewForPosition(int position, boolean dryRun){
 2    if(holder == null){
 3        // 从mAttachedScrap,mCachedViews获取ViewHolder
 4        holder = getScrapViewForPosition(position,INVALID,dryRun); // 此处获得的View不需要bind
 5    }
 6    final int type = mAdapter.getItemViewType(offsetPosition);
 7    if (mAdapter.hasStableIds()) { // 默认为false
 8        holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
 9    }
10    if(holder == null && mViewCacheExtension != null){
11        final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
12        if(view != null){
13            holder = getChildViewHolder(view);
14        }
15    }
16    if(holder == null){
17        holder = getRecycledViewPool().getRecycledView(type);
18    }
19    if(holder == null){  // 没有缓存,则创建
20        holder = mAdapter.createViewHolder(RecyclerView.this, type); // 调用onCreateViewHolder()
21    }
22    if(!holder.isBound() || holder.needsUpdate() || holder.isInvalid()){
23        mAdapter.bindViewHolder(holder, offsetPosition);
24    }
25    return holder.itemView;
26}

从上述实现可以看出,依次从mAttachedScrap, mCachedViews, mViewCacheExtension, mRecyclerPool寻找可复用的ViewHolder,如果是从mAttachedScrap或mCachedViews中获取的ViewHolder,则不会调用onBindViewHolder(),mAttachedScrap和mCachedViews也就是我们所说的Scrap Heap;而如果从mViewCacheExtension或mRecyclerPool中获取的ViewHolder,则会调用onBindViewHolder()。

RecyclerView局部刷新的实现原理也是基于RecyclerView的回收机制,即能直接复用的ViewHolder就不调用onBindViewHolder()。

参考资料

1、强大而灵活的RecyclerView Adapter: https://github.com/CymChad/BaseRecyclerViewAdapterHelper

2、 RecyclerView ins and outs - Google I_O 2016