RecyclerView 的应用

418 阅读7分钟

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

使用 RecyclerView

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

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="10dp"
    tools:context=".MainActivity">
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <Button
            android:text="添加数据"
            android:onClick="onClickAddData"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"/>
        <Button
            android:text="横向排列"
            android:onClick="onClickHorizontal"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_marginStart="3dp"
            android:layout_height="wrap_content"/>
        <Button
            android:text="反向展示"
            android:onClick="onClickReverse"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_marginStart="3dp"
            android:layout_height="wrap_content"/>
    </LinearLayout>

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <Button
            android:id="@+id/btn_linear_layout"
            android:text="线性布局"
            android:onClick="onChangeLayout"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"/>
        <Button
            android:id="@+id/btn_grid_layout"
            android:text="网格布局"
            android:onClick="onChangeLayout"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_marginStart="3dp"
            android:layout_height="wrap_content"/>
        <Button
            android:id="@+id/btn_staggered_grid_layout"
            android:text="瀑布流布局"
            android:onClick="onChangeLayout"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_marginStart="3dp"
            android:layout_height="wrap_content"/>
    </LinearLayout>

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <Button
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:text="插入一条数据"
            android:onClick="onInsertDataClick"/>

        <Button
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:text="删除一条数据"
            android:layout_marginStart="3dp"
            android:onClick="onRemoveDataClick"/>
    </LinearLayout>



    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>

item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:background="#CDDC39"
    android:layout_margin="4dp"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ImageView
        android:padding="5dp"
        android:id="@+id/iv"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:scaleType="fitXY"/>

    <TextView
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:textColor="@color/black"
        android:gravity="center_vertical"
        android:layout_marginStart="8dp"/>
</LinearLayout>

MyRecycleViewAdapter.java

/**
 * 1、继承RecycleView.Adapter
 * 2、绑定ViewHolder
 * 3、实现Adapter的相关方法
 */
public class MyRecycleViewAdapter extends RecyclerView.Adapter<MyRecycleViewAdapter.MyViewHolder> {

    private final Context context;
    private final RecyclerView recyclerView;
    private List<String> dataSource;
    private OnItemClickListener listener;

    public MyRecycleViewAdapter(Context context, RecyclerView recyclerView){
        this.context = context;
        this.recyclerView = recyclerView;
        this.dataSource = new ArrayList<>();
    }

    public void setDataSource(List<String> dataSource) {
        this.dataSource = dataSource;
        notifyDataSetChanged();
    }

    public void setListener(OnItemClickListener listener) {
        this.listener = listener;
    }

    // 创建并返回ViewHolder
    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new MyViewHolder(LayoutInflater.from(context).inflate(R.layout.item, parent, false));
    }

    // 通过ViewHolder绑定数据
    @Override
    public void onBindViewHolder(@NonNull MyRecycleViewAdapter.MyViewHolder holder, int position) {
        holder.imageView.setImageResource(getIcon(position));
        holder.textView.setText(dataSource.get(position));
        LinearLayout.LayoutParams params;
        if(StaggeredGridLayoutManager.class.equals(recyclerView.getLayoutManager().getClass())){
            int randomHeight = getRandomHeight();
            // 只在瀑布流布局中使用随机高度
            params = new LinearLayout.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    randomHeight < 50 ? dp2px(context, 50f): randomHeight
            );
        }else{
            params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        }
        params.gravity = Gravity.CENTER;
        holder.textView.setLayoutParams(params);

        holder.itemView.setOnClickListener(v -> listener.onItemClick(position));
    }

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

    // 返回数据数量
    @Override
    public int getItemCount() {
        return dataSource.size();
    }

    // 返回不同的随机ItemView高度
    private int getRandomHeight(){
        return (int)(Math.random() * 500);
    }

    // 根据不同的position选择一个图片
    private int getIcon(int position){
        switch (position % 5){
            case 0:
                return R.drawable.ic_4k;
            case 1:
                return R.drawable.ic_5g;
            case 2:
                return R.drawable.ic_360;
            case 3:
                return R.drawable.ic_adb;
            case 4:
                return R.drawable.ic_alarm;
            default:
                return 0;
        }
    }

    // 添加一条数据
    public void addData (int position) {
        dataSource.add(position, "插入的数据");
        notifyItemInserted(position);
        // 刷新ItemView
        notifyItemRangeChanged(position, dataSource.size() - position);
    }

    // 删除一条数据
    public void removeData (int position) {
        dataSource.remove(position);
        notifyItemRemoved(position);

        // 刷新ItemView
        notifyItemRangeChanged(position, dataSource.size() - position);
    }

    static class MyViewHolder extends RecyclerView.ViewHolder {
        ImageView imageView;
        TextView textView;
        public MyViewHolder(@NonNull View itemView) {
            super(itemView);
            imageView = itemView.findViewById(R.id.iv);
            textView = itemView.findViewById(R.id.tv);
        }
    }

    interface OnItemClickListener {
        void onItemClick(int position);
    }
}

MainActivity.java

public class MainActivity extends AppCompatActivity {

    private RecyclerView recyclerView;
    private MyRecycleViewAdapter adapter;
    private LinearLayoutManager linearLayoutManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        recyclerView = findViewById(R.id.recycler_view);
        // 设置线性布局
        linearLayoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(linearLayoutManager);

        adapter = new MyRecycleViewAdapter(this, recyclerView);
        adapter.setListener(position -> Toast.makeText(MainActivity.this, "第" + position + "数据被点击", Toast.LENGTH_SHORT).show());
        recyclerView.setAdapter(adapter);

    }

    public void onClickAddData(View view) {
        List<String> data = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
            data.add("第" + i + "条数据");
        }
        adapter.setDataSource(data);
    }

    public void onClickHorizontal(View view) {
        linearLayoutManager.setReverseLayout(false);
        // 横向排列ItemView
        linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
        recyclerView.setLayoutManager(linearLayoutManager);
    }


    public void onClickReverse(View view) {
        // 数据反向展示
        linearLayoutManager.setReverseLayout(true);
        // 数据纵向排列
        linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(linearLayoutManager);
    }

    public void onChangeLayout(View view) {
        switch (view.getId()){
            case R.id.btn_linear_layout:
                LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
                recyclerView.setLayoutManager(linearLayoutManager);
                break;
            case R.id.btn_grid_layout:
                GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 2);
                recyclerView.setLayoutManager(gridLayoutManager);
                break;
            case R.id.btn_staggered_grid_layout:
                StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
                recyclerView.setLayoutManager(staggeredGridLayoutManager);
                break;
        }
    }

    // 插入一条数据
    public void onInsertDataClick (View v) {
        adapter.addData(1);
    }

    // 删除一条数据
    public void onRemoveDataClick (View v) {
        adapter.removeData(1);
    }
}
布局类效果
LinearLayoutManager以垂直或水平滚动列表方式显示项目
GridLayoutManager在网格中显示项目
StaggeredGridLayoutManager在分散对齐网格中显示项目

上述代码解析

RecyclerView 使用步骤

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

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

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

// 映射ItemLayoutId,创建VH并返回
onCreateViewHolder(ViewGroup parent, int viewType)

// 为Holder设置指定数据
onBindViewHolder(VH holder, int position)

// 返回Item的个数
getItemCount()

RecyclerView 局部刷新

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

// 添加一条数据
public void addData (int position) {
  dataSource.add(position, "插入的数据");

  notifyItemInserted(position);
  // 刷新ItemView
  notifyItemRangeChanged(position, dataSource.size() - position);
}

// 删除一条数据
public void removeData (int position) {
  dataSource.remove(position);
  notifyItemRemoved(position);

  // 刷新ItemView
  notifyItemRangeChanged(position, dataSource.size() - position);
}

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

public void updateItemView(ListView listview, int position, Data data){
    int firstPos = listview.getFirstVisiblePosition();
    int lastPos = listview.getLastVisiblePosition();

    // 可见才更新,不可见则在getView()时更新
    if(position >= firstPos && position <= lastPos){
        //listview.getChildAt(i)获得的是当前可见的第i个item的view
        View view = listview.getChildAt(position - firstPos);
        VH vh = (VH)view.getTag();
        vh.text.setText(data.text);
    }
}

Item 的点击 / 长按事件

interface OnItemClickListener {
    void onItemClick(int position);
}

private OnItemClickListener listener;

public void setListener(OnItemClickListener listener) {
    this.listener = listener;
}

......

public void onBindViewHolder(@NonNull MyRecycleViewAdapter.MyViewHolder holder, int position) {
    ......
    holder.itemView.setOnClickListener(v -> listener.onItemClick(position));
    holder.itemView.setOnLongClickListener(v -> {
        listener.onItemLongClick(position);
        return false;
    });
}

其他说明

1、dp 单位转 px 单位:

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

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(),实现如下:

void layoutChildren(){
    // 1. 如果数据被改变了,则将所有ItemView回收至scrapView  
    // 而RecyclerView会根据情况放入Scrap Heap或RecyclePool,否则回收至mActiveViews
    if(dataChanged) {
        for (int i = 0; i < childCount; i++) {
            recycleBin.addScrapView(getChildAt(i), firstPosition+i);
        }
    }else {
    	recycleBin.fillActiveViews(childCount, firstPosition);
    }

    // 2. 填充
    switch(){
        case LAYOUT_XXX:
            fillXxx();
            break;
        case LAYOUT_XXX:
            fillXxx();
            break;
    }

    // 3. 回收多余的activeView
    mRecycler.scrapActiveViews();
}

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

View makeAndAddView(){
    if(!mDataChanged) {
        child = mRecycler.getActiveView(position);
        if (child != null) {
            return child;
        }
    }
    child = obtainView(position, mIsScrap);
    return child;
}

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

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

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

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

class RecycleBin{
    private View[] mActiveViews;           // 存储屏幕上的View
    private ArrayList<View>[] mScrapViews; // 每个item type对应一个ArrayList
    private int mViewTypeCount;            // item type的个数
    private ArrayList<View> mCurrentScrap; // mScrapViews[0]
    View getScrapView(int position) {
        final int whichScrap = mAdapter.getItemViewType(position);
        if(whichScrap < 0) {
            return null;
        }
        if(mViewTypeCount == 1) {
             return retrieveFromScrap(mCurrentScrap, position);
        }else if (whichScrap < mScrapViews.length) {
            return retrieveFromScrap(mScrapViews[whichScrap], position);
        }
        return null;
    }
    private View retrieveFromScrap(ArrayList<View> scrapViews, int position){
        int size = scrapViews.size();
        if(size > 0){
            return scrapView.remove(scrapViews.size() - 1);  // 从回收列表中取出最后一个元素复用
        }else{
            return null;
        }
    }
}

RecyclerView 回收机制

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

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

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

View getViewForPosition(int position, boolean dryRun){
    if(holder == null){
        // 从mAttachedScrap,mCachedViews获取ViewHolder
        holder = getScrapViewForPosition(position,INVALID,dryRun); // 此处获得的View不需要bind
    }
    final int type = mAdapter.getItemViewType(offsetPosition);
    if (mAdapter.hasStableIds()) { // 默认为false
        holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
    }
    if(holder == null && mViewCacheExtension != null){
        final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
        if(view != null){
            holder = getChildViewHolder(view);
        }
    }
    if(holder == null){
        holder = getRecycledViewPool().getRecycledView(type);
    }
    if(holder == null){  // 没有缓存,则创建
        holder = mAdapter.createViewHolder(RecyclerView.this, type); // 调用onCreateViewHolder()
    }
    if(!holder.isBound() || holder.needsUpdate() || holder.isInvalid()){
        mAdapter.bindViewHolder(holder, offsetPosition);
    }
    return holder.itemView;
}

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

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

参考资料

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

2、RecyclerView ins and outs - Google I_O 2016