前言
众所周知,RecyclerView是一个功能强大的,用于替代ListView的控件。话虽如此,我在项目中的大多数情况下仍只是将其当做一个加强版的ListView使用,很少有深入使用其高级的功能,不过随着需求的增加,深入学习RecyclerView的高级功能也是必不可少的,因此,谨以此文记录我所见所闻的RecyclerView,方便日后复习查阅。
基础
基础使用
RecyclerView可以说是开发中的常客,相信大部分开发者都能轻松地完成基本使用下的代码,甚至由于经常使用而形成了肌肉记忆,实际上RecyclerView的基本使用确实是十分公式化的,具体可以分为以下步骤: 1. 添加依赖
dependencies {
...
implementation 'com.google.android.material:material:1.2.1'
}
- 在布局中添加RecyclerView
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
-
编写item布局
这个item布局对应的是列表中需要展示的每个条目的布局,注意其父容器的layout_height属性的值一般不设置为match_parent,否则会导致展示结果占据大量位置,观感较差,这是一种十分容易出现的错误。
<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="60dp"
android:gravity="center"
android:background="#ffffff">
<TextView
android:id="@+id/item_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#000000"
tools:text="item"/>
</LinearLayout>
-
编写适配器(Adapter)
使用RecyclerView最关键的一步,可以说RecyclerView的大部分逻辑功能都在此实现。这里的关注点就比较多了。
首先是ViewHolder:
这是一种性能优化的策略,RecyclerView之所以能快速展示大量数据,ViewHolder可以说是功不可没。从字面上理解,不难看出它的功能就是容纳(hold)一个视图(View),一个视图好端端的,为什么要特意再使用ViewHolder容纳呢?举个例子就很容易理解了:当我们的列表中有一百万个视图要展示时,如果我们直接加载这一百万个视图,必然会出现OOM现象,但是使用ViewHolder则不会,这是因为使用此策略,我们仅创建很少的ViewHolder来容纳View,当我们滑动列表时,实际上就是在不断复用这些ViewHolder,极大的增加了效率。至此,如果仍存有疑惑,可以至文章末尾查看相关测试。 道理讲了一大堆,具体的使用实际上就是初始化需要展示数据的控件,然后在onBindViewHolder方法中设置对应的属性即可。
其次是三个必须重写的重要方法: - onCreateViewHolder:用于创建ViewHolder,此处加载的布局即为步骤2中创建的布局。 - onBindViewHolder:用于绑定数据和事件,参数postion代表位置。 - getItemCount:用于确认子项的数量,即列表总共有多少项,一般返回数据的大小。
具体的使用看代码也十分容易理解。
结合到代码,可以理清基本的步骤: 1、 创建一个MyAdapter类,继承RecyclerView.Adapter并定义一个内部类ViewHolder。 2、 重写onCreateViewHolder、onBindViewHolder、getItemCount方法。 3、 提供一个设置数据的方法,可以是构造方法,也可以是setData方法。 4、 其他实现,如数据变更后的刷新,根据需求定义即可。
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
//需要展示的数据保存在这里
private List<String> mContent;
public MyAdapter(List<String> content){
mContent = content;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout,parent,false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
String text = mContent.get(position);
holder.itemText.setText(text);
}
@Override
public int getItemCount() {
//返回数据的大小
return mContent.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
TextView itemText;
ViewHolder(@NonNull View itemView) {
super(itemView);
itemText = itemView.findViewById(R.id.item_text);
}
}
}
-
初始化RecyclerView
为RecyclerView设置布局管理器和适配器。由于RecyclerView没有ListView和GridView那样明确的布局方式,因此需要设置布局管理器,这当然也是优势之一,可以一己之力适配多种布局方式。RecyclerView提供了三种布局方式:LinearLayoutManager(线性布局)、GridLayoutManager(网格布局)和StaggeredGridLayoutManager(瀑布流布局)。
//初始化组件
RecyclerView recyclerView = findViewById(R.id.recycler_view);
//设置布局管理器(LayoutManager)
recyclerView.setLayoutManager(new LinearLayoutManager(this));
//设置适配器(Adapter)和数据
List<String> content = new ArrayList<>();
for (int i = 1; i < 11; i++){
content.add("item " + i);
}
MyAdapter myAdapter = new MyAdapter(content);
recyclerView.setAdapter(myAdapter);
调整间距与添加装饰
为了让列表看起来更美观,往往还需要对每个item调整间距或添加装饰。比较简单的方法就是在item的布局中预先设置好margin属性,这种方法在线性布局中的使用效果还不错,但在网格布局和瀑布流布局中就显得不够灵活了。这时就要用到RecyclerView.addItemDecoration(ItemDecoration decor)方法了。
添加默认的装饰
可以看到,此方法需要一个ItemDecoration对象,使用由系统提供的它的子类DividerItemDecoration即可。
//添加分割线
recyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
默认的装饰太单调,想整点花的?可以,使用DividerItemDecoration.setDrawble(Drawable drawable)方法传入一个Drawable对象来代替默认的装饰。此处用一个简单的绿色的矩形展示效果。
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#00ff00"/>
<size android:height="2dp" />
</shape>
//来点花里胡哨的
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(this,DividerItemDecoration.VERTICAL);
dividerItemDecoration.setDrawable(getResources().getDrawable(R.drawable.divider_green));
recyclerView.addItemDecoration(dividerItemDecoration);
自定义装饰
如果默认的装饰不足以满足需求,那么就自定义一个装饰吧。在使用默认装饰时,系统提供了DividerItemDecoration类,它是基础自ItemDecoration类的,那么我们也创建一个MyDecoration类继承它,并完善相关功能即可。 使用快捷键Ctrl+O查看需要重写的方法,可以看到,除去已废弃的和构造方法,主要有三个方法:
- getItemOffsets(),获取Item的偏移量,调整参数中outRect的相关值,可以实现类似padding的效果。
- onDraw(),用这种方法绘制的任何内容都将在绘制项目视图之前绘制,绘图的内容将出现在视图下方。
- onDrawOver(),用这种方法绘制的任何内容都将在绘制项目视图之后绘制,绘图的内容会出现在视图上方。
直接看效果更容易理解,首先是getItemOffsets()方法,为了方便观察,把值设置得大一些:
//自定义装饰
class MyDecoration extends RecyclerView.ItemDecoration{
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
//设置每个item下方、左侧和右侧的间距
outRect.bottom = 20;
outRect.left = 40;
outRect.right = 60;
}
}
可以看到,结果类似实现了padding的效果。如果仅设置每个item的bottom间距,实现的效果与默认装饰类似。
接着看onDraw()方法,这相对复杂一些,因为涉及装饰的绘制和位置的计算,以一个简单的例子为例:
//自定义装饰
class MyDecoration extends RecyclerView.ItemDecoration{
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
//设置间距
outRect.bottom = 20;
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
//实例化一个蓝色的画笔
Paint paint = new Paint();
paint.setColor(Color.BLUE);
//获取item的数量
int count = parent.getChildCount();
//此处绘制一个矩形,设置其位置属性
//由于列表是垂直分布的,所以每个要绘制的矩形左坐标和右坐标是一致的
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
//顶部坐标和底部坐标要根据item单独计算
for (int i = 0; i < count; i++){
View view = parent.getChildAt(i);
//要绘制的矩形的顶部恰好是item的底部,其底部则是此值 + 间距
float top = view.getBottom();
float bottom = view.getBottom() + 20;
//绘制矩形
c.drawRect(left,top,right,bottom,paint);
}
}
}
可以看到,在每个item的底部绘制了高度为20px的蓝色矩形,灵活覆写onDraw方法可以实现更好的效果。
最后是onDrawOver()方法,此方法的使用与onDraw()方法一致,唯一的区别就是绘图的内容会出现在视图上方,如果知道图层的概念应该就不难理解。简便起见,getItemOffsets()和onDraw()方法不做修改。
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(c, parent, state);
Paint paint = new Paint();
paint.setColor(Color.RED);
int count = parent.getChildCount();
float left = parent.getRight() - 40;
float right = parent.getRight();
for (int i = 0; i < count; i++){
View view = parent.getChildAt(i);
float top = view.getTop();
//会覆盖掉蓝色装饰
float bottom = view.getBottom() + 20;
c.drawRect(left,top,right,bottom,paint);
}
}
很显然,新绘制的红色矩形覆盖了部分蓝色的矩形。
分类列表
有时我们需要对列表中的item进行分类展示,比如通讯录、好友列表。想要实现此功能,无非就是在适配器中根据类型进行内容的加载。可以这样理解:之前适配器中都只加载了一种布局,即item的布局。现在我们加载两种布局,一种为标题布局,一种为内容布局,并根据要求选择性地进行加载,最终展示出来的就是我们想要的结果。
以通讯录为例,首先实现两个布局,分别用于展示联系人姓名首字母和联系人基础信息:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tool="http://schemas.android.com/tools"
android:padding="10dp">
<TextView
android:id="@+id/relation_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="#000000"
tool:text="A"/>
</FrameLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tool="http://schemas.android.com/tools"
android:orientation="horizontal"
android:padding="10dp">
<ImageView
android:id="@+id/relation_profile"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@mipmap/ic_launcher_round"/>
<TextView
android:id="@+id/relation_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:textColor="#000000"
android:textSize="18sp"
tool:text="张三"/>
</LinearLayout>
接着实现适配器,看似内容很多,实际上大部分与基础使用无异。分几点详细说明: 1. 为简便起见,数据采用Map<Integer,String>的类型,实际使用时建议用Bean类代替。它存储两个键值对,键FLAG_IS_TITLE对应的值指代它是否为标题,键CONTENT对应的值为它的内容。 2. 相较于基础使用,此处新增了一个方法getItemViewType(int position),这个方法就是用于返回类型的。 3. onCreateViewHolder(ViewGroup parent, int viewType)中的参数viewType指的就是上述方法中返回的int值,在此需要根据类型进行ViewHolder的创建,所以我们需要两个ViewHolder(ItemHolder和TitleHolder)。
public class RelationAdapter extends RecyclerView.Adapter<RelationAdapter.ViewHolder> {
private List<Map<Integer,String>> mData = new ArrayList<>();
//指代类型的常量
private static final int VIEW_TYPE_TITLE = 0;
private static final int VIEW_TYPE_ITEM = 1;
private int FLAG_IS_TITLE = 0;
private int CONTENT = 1;
public RelationAdapter(List<Map<Integer,String>> data){
mData = data;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ViewHolder viewHolder = null;
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
switch (viewType){
case VIEW_TYPE_TITLE:
viewHolder = new TitleHolder(inflater.inflate(R.layout.relation_title,parent,false));
break;
case VIEW_TYPE_ITEM:
viewHolder = new ItemHolder(inflater.inflate(R.layout.relation_item,parent,false));
break;
}
return viewHolder;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
if ("true".equals(mData.get(position).get(FLAG_IS_TITLE))){
holder.titleTv.setText(mData.get(position).get(CONTENT));
} else {
holder.itemTv.setText(mData.get(position).get(CONTENT));
}
}
@Override
public int getItemViewType(int position) {
if (holder instanceof TitleHolder){
return VIEW_TYPE_TITLE;
} else {
return VIEW_TYPE_ITEM;
}
}
@Override
public int getItemCount() {
return mData.size();
}
public class ViewHolder extends RecyclerView.ViewHolder{
TextView itemTv;
TextView titleTv;
ViewHolder(@NonNull View itemView) {
super(itemView);
}
}
public class ItemHolder extends ViewHolder {
public ItemHolder(@NonNull View itemView) {
super(itemView);
itemTv = itemView.findViewById(R.id.relation_name);
}
}
public class TitleHolder extends ViewHolder {
public TitleHolder(@NonNull View itemView) {
super(itemView);
titleTv = itemView.findViewById(R.id.relation_title);
}
}
}
最后只需初始化RecyclerView并设置适配器即可:
public class ThirdActivity extends AppCompatActivity {
private List<Map<Integer,String>> mData = new ArrayList<>();
private Map<Integer,String> map = new HashMap<>();
private static final int FLAG_IS_TITLE = 0;
private static final int CONTENT = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_third);
//初始化组件
RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
//初始化数据
map = new HashMap<>();
map.put(FLAG_IS_TITLE,"true");
map.put(CONTENT,"L");
mData.add(map);
map = new HashMap<>();
map.put(FLAG_IS_TITLE,"false");
map.put(CONTENT,"李四");
mData.add(map);
map = new HashMap<>();
map.put(FLAG_IS_TITLE,"false");
map.put(CONTENT,"李五");
mData.add(map);
map = new HashMap<>();
map.put(FLAG_IS_TITLE,"true");
map.put(CONTENT,"Z");
mData.add(map);
map = new HashMap<>();
map.put(FLAG_IS_TITLE,"false");
map.put(CONTENT,"张三");
mData.add(map);
//初始化适配器
RelationAdapter relationAdapter = new RelationAdapter(mData);
recyclerView.setAdapter(relationAdapter);
}
}
进阶
拖拽和滑动
以基础使用的代码为主实现此效果,下面是最终效果图:
要实现此效果,首先要认识一个新的类:ItemTouchHelper。从它的名字不难看出,它是一个辅助item触摸的帮助类。而我们要做的事,就是继承ItemTouchHelper.Callback这个抽象类,并重写其中的抽象方法,最后将其与我们的RecyclerView绑定。从整个过程看,其实与实现Adapter是类似的。
先介绍一下三个重要方法:
- getMovementFlags:此方法返回一个整数类型的标识,指定item的那些移动行为是被允许的。通过makeMovementFlags(int dragFlags, int swipeFlags)方法返回,两个参数分别指定拖拽方向和滑动方向。
- onMove:拖拽操作的回调方法,它有两个ViewHolder类型的参数,指代被拖动的ViewHolder和目标位置的ViewHolder,返回值为布尔类型,表示item是否发生了位置交换。
- onSwiped:滑动操作的回调方法,它有一个ViewHolder类型和一个int类型的参数,分别指代被滑动的ViewHolder和滑动的方向。
大概了解过三个方法后,便可以根据需求着手自定义这个类了。注释比较清晰,就不过多叙述了。
public class MyItemTouchCallback extends ItemTouchHelper.Callback {
private final ItemTouchHelperCallback helperCallback;
public MyItemTouchCallback(ItemTouchHelperCallback helperCallback){
this.helperCallback = helperCallback;
}
/**
* 返回一个整数类型的标识,指定Item的哪种移动行为是被允许的
* 通过makeMovementFlags(int dragFlags, int swipeFlags)方法返回
* dragFlags指定拖拽方向,swipeFlags指定滑动方向
*/
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
/**
* ItemTouchHelper.UP 向上
* ItemTouchHelper.DOWN 向下
* ItemTouchHelper.LEFT 向左
* ItemTouchHelper.RIGHT 向右
* ItemTouchHelper.START 从右向左
* ItemTouchHelper.END 从左向右
* 如果某个值传0,表示不触发该操作,此处设置支持上下拖拽和向右滑动
*/
return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN,ItemTouchHelper.END);
}
/**
* 拖拽操作的回调
* @param viewHolder 被拖动的ViewHolder
* @param target 目标位置的ViewHolder
* @return 如果item发生位置交换,返回true,否则返回false
*/
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
helperCallback.onMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
return true;
}
/**
* 滑动操作的回调
* @param viewHolder 滑动的ViewHolder
* @param direction 滑动的方向
*/
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
helperCallback.onItemDelete(viewHolder.getAdapterPosition());
}
/**
* 自定义接口用于方法回调
*/
public interface ItemTouchHelperCallback{
//滑动删除
void onItemDelete(int position);
//拖拽
void onMove(int fromPosition, int toPosition);
}
接着,让Adapter实现ItemTouchHelperCallBack接口,并实现相关方法。
@Override
public void onItemDelete(int position) {
mContent.remove(position);
notifyItemRemoved(position);
}
@Override
public void onMove(int fromPosition, int toPosition) {
Collections.swap(mContent,fromPosition,toPosition);
notifyItemMoved(fromPosition,toPosition);
}
最后,将ItemTouchHelper与RecyclerView绑定即可。
//绑定itemTouchHelper
ItemTouchHelper.Callback callback = new MyItemTouchCallback(myAdapter);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(recyclerView);
至此,本文的正片内容暂且告一段落了。
个人总结,内容难免存在纰漏,细节部分不能做到尽善尽美,请各位读者海涵!
测试部分
该部分内容用于测试或说明一些开发中遇到的问题和现象。
ViewHolder
前文提到,当RecyclerView中需要展示大量的数据时,会通过创建和复用较少的ViewHolder来提升效率。只需通过对比数据总量和onCreateViewHolder的调用次数即可证明。以基础使用部分的代码为主,为了更加明显地展示结果,数据量增加到100,并输出各种情况下onCreateViewHolder和onBindViewHolder的调用次数。
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout,parent,false);
//输出onCreateViewHolder的调用次数
Log.e("MyAdapter","onCreateViewHolder:" + ++create_count);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
String text = mContent.get(position);
holder.itemText.setText(text);
//输出onBindViewHolder的调用次数
Log.e("MyAdapter","onBindViewHolder:" + ++bind_count);
}
启动应用,不进行任何操作时的输出结果:
可以看到,无论是onCreateViewHolder还是onBindViewHolder都只调用了11次,这也表示此时RecyclerView能显示11个item。
接着,滑动列表,查看输出结果:
结果显而易见,onCreateViewHolder在调用了15次后不再调用,即只创建了15个ViewHolder,而onBindViewHolder是在不断被调用的,这也是正常的结果,因为一个新的item进入屏幕,肯定是需要绑定数据和事件的。结论就是,ViewHolder确实能通过复用提高效率。至于ViewHolder的创建数量,由于没看过源码,只能说,至少从测试中可以看出,它的创建数量与RecyclerView中可以同时展示(能被用户看见)的item最大数量有关。
addItemDecoration
关于RecyclerView.addItemDecoration(ItemDecoration decor)方法,相信大家已经发现了,这个方法以add而不是set开头,这也就说明可以添加多个ItemDecoration,其内部则以数组的形式存储,那么具体的展示情况如何呢,不妨测试看看。为方便起见,测试使用DividerItemDecoration类,添加数个颜色、高度不一的矩形。
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#ff0000"/>
<size android:height="12dp" />
</shape>
对应格式如上,分别为绿色,4dp;蓝色,8dp,红色,12dp。为RecyclerView添加上这些装饰,并观察结果
//测试
DividerItemDecoration divider1 = new DividerItemDecoration(this,DividerItemDecoration.VERTICAL);
divider1.setDrawable(getResources().getDrawable(R.drawable.divider_green));
DividerItemDecoration divider2 = new DividerItemDecoration(this,DividerItemDecoration.VERTICAL);
divider2.setDrawable(getResources().getDrawable(R.drawable.divider_blue));
DividerItemDecoration divider3 = new DividerItemDecoration(this,DividerItemDecoration.VERTICAL);
divider3.setDrawable(getResources().getDrawable(R.drawable.divider_red));
recyclerView.addItemDecoration(divider1);
recyclerView.addItemDecoration(divider2);
recyclerView.addItemDecoration(divider3);
正序添加时,结果如图,只出现了红色和灰色(背景色)的装饰,且它们的高度一致。
再看逆序添加时的结果:
//测试
recyclerView.addItemDecoration(divider3);
recyclerView.addItemDecoration(divider2);
recyclerView.addItemDecoration(divider1);
至此,不涉及源码,但可以得出结论:RecyclerView的装饰可以添加多个,且它们有层级关系,越后添加的装饰会覆盖掉之前的装饰,并且总高度为各个装饰的高度之和。