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