编辑
2021-01-25
客户端技术
00
请注意,本文编写于 981 天前,最后修改于 113 天前,其中某些信息可能已经过时。

目录

使用RecyclerView
上述代码解析
RecyclerView使用步骤
RecyclerView局部刷新
Item的点击/长按事件
其他说明
ListView和RecyclerView对比
回收机制分析
ListView回收机制
RecyclerView回收机制
参考资料

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

使用RecyclerView

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

activity_main.xml:

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
<?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

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

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中实现三个方法:

java
// 映射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) 都是使用的局部刷新:

java
// 添加一条数据 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要完成局部刷新就稍微复杂一点:

java
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的点击/长按事件

java
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单位:

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

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

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

java
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,具体实现如下:

java
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()方法,因此此处介绍该方法的实现:

java
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: https://github.com/CymChad/BaseRecyclerViewAdapterHelper

2、RecyclerView ins and outs - Google I_O 2016

本文作者:Tim

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!