2

RecyclerView的应用

 3 years ago
source link: https://zouchanglin.cn/3653446494.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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 在分散对齐网格中显示项目

NviuQ3e.gif!mobile

上述代码解析

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()。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK