Android 学习笔记 —— RecyclerView 的使用
RecyclerView 的使用
ListView 的缺点很明显,只能实现数据纵向滚动效果,拓展性也不够好。为此,Android 提供了一个更强大的滚动控件 —— RecyclerView。ListView 能做到的,RecyclerView 也能做到,而且还能做得更好,所以更推荐使用 RecyclerView。
使用步骤:
-
与 ListView 一样,使用 RecyclerView 也需要在布局文件中添加相应的标签,注意它是在
androidx.recyclerview.widget
包下的:<androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" />
-
根据实际需求自定义一个 Item 布局(以联系人列表为例)。
<LinearLayout ... > <ImageView android:id="@+id/iv_contact_item_avatar" ... /> <TextView android:id="@+id/tv_contact_item_name" ... /> </LinearLayout>
-
定义数据源实体类 ContactEntity。
-
创建自定义适配器类 ContactAdapter,在 ContactAdapter 中定义的一个静态内部类 ViewHolder 并继承自
RecyclerView.ViewHolder
,然后创建与父类匹配的构造函数。public class ContactAdapter { static class ViewHolder extends RecyclerView.ViewHolder{ public ViewHolder(@NonNull View itemView) { super(itemView); } } }
-
让 ContactAdapter 类继承自
RecyclerView.Adapter
并指定泛型为ContactAdapter.ViewHolder
,然后实现其方法。public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ViewHolder> { static class ViewHolder extends RecyclerView.ViewHolder { // 从名字就可以知道传入参数是子项的 Item 视图 public ViewHolder(@NonNull View itemView) { super(itemView); } } /** * 用于创建 ViewHolder 实例 * @param parent 新视图绑定到适配器后将被添加到的视图组,这里指的其实就是 RecyclerView * @param viewType 新视图的类型,默认为 0。如果 Item 存在多种表现形式,应重写 getItemViewType() 方法给每种形式指定唯一值 * @return 一个新的持有给定视图类型视图的 ViewHolder */ @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return null; } /** * 用于对 RecyclerView 子项的数据进行赋值,在每个子项需要显示到屏幕时执行 * @param holder 需要在对应位置更新显示 Item 内容的 ViewHolder * @param position Item 所需数据在适配器的数据中的对应位置 */ @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { } /** * RecyclerView 子项的数量,返回多少就显示多少 */ @Override public int getItemCount() { return 0; } }
这是创建好这个自定义适配器类的最初状态。看上去比 ListView 更复杂,但实际比 ListView 更好理解。另外,适配器中还需要定义用于存放数据源的对象和 RecyclerView 所在的上下文 Context,同时也要添加对应的构造函数。
public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ViewHolder> { private final List<ContactEntity> data; // 对应数据源 List private final LayoutInflater inflater; // 对应 Context 的布局加载器 public ContactAdapter(Context context, List<ContactEntity> data) { this.data = data; this.inflater = LayoutInflater.from(context); } // ... }
-
给内部静态类 ViewHolder 添加对应的控件属性,并在构造函数中对其实例化。
public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ViewHolder> { // ... static class ViewHolder extends RecyclerView.ViewHolder { private ImageView avatar; private TextView name; public ViewHolder(@NonNull View itemView) { super(itemView); // 调用父类的构造函数 avatar = (ImageView) itemView.findViewById(R.id.iv_contact_item_avatar); name = (TextView) itemView.findViewById(R.id.tv_list_item_contact); } } }
-
编写 RecyclerView.Adapter 要求实现的方法。
public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ViewHolder> { // ... @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { // 使用布局加载器的 inflate 方法加载 Item 项的布局文件并转化为 View 对象 // 第一个参数为 Item 布局文件的资源 ID // 第二个参数可以给 Item 项指定一个父视图,使用参数中的 parent 即可 // 第三个参数用来确认是否将 View 添加到父视图中,false 代表只让父视图中的 layout 属性生效,但不会将 View 添加到父视图中 // 这里不能设置为 true,因为 ItemView 有父视图那将不能再添加到 RecyclerView 中,该页面也会打不开 View view = inflater.inflate(R.layout.contact_item, parent, false); // 使用 ItemView 构造一个 ViewHolder 对象并返回 return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { ContactEntity entity = data.get(position); // 使用 ViewHolder 绑定数据 holder.avatar.setImageResource(entity.getAvatar()); holder.name.setText(entity.getName()); } @Override public int getItemCount() { return data.size(); // 直接返回数据源 List 的大小即可 } }
-
在 Avtivity 中创建一个访问指定 Context 资源的 LinearLayoutManager 布局管理器对象,使用
setLayoutManager()
方法将布局管理器设置到 RecyclerView 中。// 获取 RecyclerView 对象 RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); // 创建一个访问指定 Context 资源的布局管理器 LinearLayoutManager layoutManager = new LinearLayoutManager(this); // 将布局管理器对象与 RecyclerView 绑定 recyclerView.setLayoutManager(layoutManager);
LinearLayoutManager 可以用来指定 RecyclerView 的布局方式为线性布局,同时也可以对该线性布局进行一些设置。
LinearLayoutManager layoutManager = new LinearLayoutManager(this); // 设置布局排列方式为水平方向,默认为纵向排列 VERTICAL layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); // 设置布局内 Item 顺序为反向,默认 false。 layoutManager.setReverseLayout(true); // 这两个设置也可以直接使用构造函数指定 LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, true);
-
在 Avtivity 中创建自定义适配器对象,绑定数据源。使用
setAdapter()
方法将适配器设置到 RecyclerView 中。// 假装是从网络上获取到的数据 List<ContactEntity> data = new ArrayList<>(); for (int i = 0; i < 50; i++) { ContactEntity entity = new ContactEntity(); if (i % 2 == 0) { entity.setAvatar(R.mipmap.ic_launcher); entity.setName("ic_launcher" + i); } else { entity.setAvatar(R.mipmap.ic_launcher_round); entity.setName("ic_launcher_round" + i); } data.add(entity); } // 创建自定义的适配器对象 ContactAdapter adapter = new ContactAdapter(this, data); // 将适配器对象与 RecyclerView 绑定 recyclerView.setAdapter(adapter);
-
在 ViewHolder 中设置行为响应事件。
/** * 请勿在 onBindViewHolder 方法中设置事件监听 * 这里采用让 ViewHolder 去实现 View.OnClickListener 接口的方式设置监听事件 */ static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { private ImageView avatar; private TextView name; public ViewHolder(@NonNull View itemView) { super(itemView); avatar = (ImageView) itemView.findViewById(R.id.iv_contact_item_avatar); name = (TextView) itemView.findViewById(R.id.tv_list_item_contact); // 在 ViewHolder 中对 Item 或其内部控件设置事件监听 itemView.setOnClickListener(this); avatar.setOnClickListener(this); name.setOnClickListener(this); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.contact_item: // TODO break; case R.id.iv_contact_item_avatar: break; case R.id.tv_list_item_contact: break; } } }
为什么 RecyclerView 中的 ItemView 没有使用 setTag() 方法?
创建 RecyclerView.ViewHolder 对象时需要传入一个 View,这个 View 就是 Item 视图。从 RecyclerView.ViewHolder 的源码可以看到,其实 ViewHolder 对象不仅缓存了 ItemView 中的控件 View 实例,还缓存了这个 ItemView。这样就没有必要使用 setTag()
方法了,也不需要像 ListView 那样回收复用 ItemView,直接回收复用 ViewHolder 会更好,事实上 RecyclerView 缓存机制也的确是这样。
RecyclerView.LayoutManager
LayoutManager 负责测量和定位 RecyclerView 中的 Item 视图,并确定何时回收用户不再可见的 Item 视图的策略。 通过更改 LayoutManager,可以使用 RecyclerView 来实现标准的垂直滚动列表、统一网格、交错网格、水平滚动集合等。
如果 LayoutManager 指定了默认构造函数或带有签名(Context, AttributeSet, int, int)的构造函数,RecyclerView 将在布局加载时实例化并设置 LayoutManager。 然后可以从 getProperties(Context, AttributeSet, int, int)
获得大多数使用的属性。 如果 LayoutManager 指定了两个构造函数,则非默认构造函数将优先。
除了 LinearLayoutManager,RecyclerView 还提供了两种内置的布局管理器,GridLayoutManager 和 StaggeredGridLayoutManager。GridLayoutManager 可用于实现网格布局,而 StaggeredGridLayoutManager 可用于实现瀑布流布局。
// 第一个参数为当前 Context
// 第二个参数为网格的行数或列数
// 第三个参数为布局排列方向
// 第四个参数为是否反转布局
GridLayoutManager layoutManager = new GridLayoutManager(this, 3, RecyclerView.HORIZONTAL, true);
// 第一个参数为瀑布流的行数或列数
// 第二个参数为布局排列方向
StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
RecyclerView 设置分割线 —— addItemDecoration() 方法
RecyclerView 成功显示出来了,但现在的显示效果看上去怪怪的。对比一下 ListView 就会发现 RecyclerView 子项之间是没有分割线的,而 ListView 自带分割线,还有两个属性 android:divider
和 android:dividerHeight
可以用来自定义分割线。
那 RecyclerView 中有这两个或类似的属性吗?答案是没有。当然,可以专门在 Item 布局中的适当地方添加一个高度/宽度为 1 的带背景的 View 作为分割线,如果 Item 有明显背景也可以通过给 Item 的最外层布局设置一个 margin 值。这两种方法是可以实现效果的,但是不够优雅。
事实上,RecyclerView 提供了一个添加装饰的方法——addItemDecoration(RecyclerView.ItemDecoration)
,该方法传入的参数类型是一个抽象类。
public abstract static class ItemDecoration {
// 调用此方法绘制的任何内容都会在 ItemView 绘制之前进行绘制,因此将会显示在 ItemView 的背后
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
}
// 调用此方法绘制的任何内容都会在 ItemView 绘制之前进行绘制,因此将会显示在 ItemView 的表面
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
@NonNull State state) {
}
// 通过 outRect.set() 为每个 Item 设置一定的偏移量,可以实现类似 padding 和 margin 的效果,主要用于绘制 Decorator
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent, @NonNull State state) {
}
}
官方有一个自带的实现类——DividerItemDecoration,但只能用于 LinearLayoutManager 的布局方式。使用起来很简单,指定上下文对象和分割线的方向构造其实例,再作为参数传入 addItemDecoration()
方法中即可实现分割线。
// 第一个参数是上下文,第二个参数是分割线对应 RecyclerView 的方向
DividerItemDecoration decoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL);
// 默认使用的是系统自带分割线样式 android.R.attr.listDivider
// 可以使用 setDrawable() 修改样式
Drawable drawable = AppCompatResources.getDrawable(this, R.drawable.divider_diy);
if (drawable != null) {
decoration.setDrawable(drawable);
}
recyclerView.addItemDecoration(decoration);
// 也可以 setOrientation() 修改分割线方向
// decoration.setOrientation(DividerItemDecoration.HORIZONTAL);
// divider_diy.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="line">
<stroke
android:width="2dp"
android:color="@android:color/darker_gray" />
<size android:height="2dp" />
</shape>
当然也可以选择自己实现 RecyclerView.ItemDecoration 抽象类。
RecyclerView 设置事件监听 —— addOnItemTouchListener() 方法
该方法的官方文档说明:添加一个 RecyclerView.OnItemTouchListener 以在触摸事件被分派到子视图或此视图的标准滚动行为之前拦截它们。
前面通过让 ViewHolder 去实现 View.OnClickListener 已经实现了在 RecyclerView 中设置行为响应事件,但其实 RecyclerView 本身并没有实现 OnItemClick 等事件监听的功能。不过 RecyclerView 提供了一个基本事件监听接口 OnItemTouchListener,同时官方也给了一个基本实现 SimpleOnItemTouchListener,但实际上只是空实现:
public static class SimpleOnItemTouchListener implements RecyclerView.OnItemTouchListener {
/** 当返回 true 时,MotionEvent 将被拦截在这一层,不会将 MotionEvent 传递给下一层 View。 */
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
return false;
}
/** 收到了onInterceptTouchEvent 的处理后,无论其返回什么,都会调用 onTouchEvent 进行处理。 */
@Override
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
}
/** 当有子 View 反对拦截 MotionEvent 的时候,会调用该方法。 */
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
}
官方推荐实现 RecyclerView.OnItemTouchListener 去自定义触摸事件,这个接口实质是利用了 View 的事件分发与拦截机制。大体思路:在 onInterceptTouchEvent()
中通过 rv.findChildViewUnder(e.getX(), e.getY())
获取当前触摸位置对应的 ItemView,根据触摸状态决定是否要把事件拦截,在拦截的时候同时添加一个回调方法,让自定义的监听器接口在这里得到回调。
public class RecyclerViewTouchListener implements RecyclerView.OnItemTouchListener {
// 定义内部接口属性
private final onItemClickListener onItemClickListener;
// 构造函数
public RecyclerViewTouchListener(RecyclerViewTouchListener.onItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
// 获取当前触摸位置下的 ItemView
View child = rv.findChildViewUnder(e.getX(), e.getY());
if (child != null) {
// 获取子视图在适配器数据中的位置
int position = rv.getChildAdapterPosition(child);
// 获取子视图在 RecyclerView 中位置
int id = rv.getChildLayoutPosition(child);
// 调用内部接口的点击方法,通过接口让外部实现具体行为
onItemClickListener.onItemClick(rv, child, position, id);
// 也可以获取与子视图对应的 ViewHolder
// RecyclerView.ViewHolder viewHolder = rv.getChildViewHolder(child);
return true; // 返回 true 对触摸事件进行拦截
}
return false;
}
@Override
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
// 定义内部接口
public interface onItemClickListener {
// 定义响应点击事件方法,传入所需参数
// 这里模仿 ListView 传入父视图,子视图,子视图在适配器数据源中的位置,子视图在父视图中的位置 ID
void onItemClick(RecyclerView recyclerView, View itemView, int position, long id);
}
}
定义好 RecyclerView.OnItemTouchListener 实现类后,就可以在对应 Activity 中使用 addOnItemTouchListener()
给 RecyclerView 添加触摸事件监听器。
recyclerView.addOnItemTouchListener(new RecyclerViewTouchListener(new RecyclerViewTouchListener.onItemClickListener() {
@Override
public void onItemClick(RecyclerView recyclerView, View itemView, int position, long id) {
// TODO
}
}));
使用上面的方式可以达到给 RecyclerView 子项添加触摸事件,但并没有动画,同时还不能处理子项中某一个控件的响应事件。