07.18 控件-RecyclerView

2024/8/14 Android 开发基础

RecyclerView 可以让您轻松高效地显示大量数据,您提供数据并定义每个列表项的外观,RecyclerView 会根据需要动态创建元素。

顾名思义,RecyclerView 会回收这些单个的列表项元素。当列表项滚动出屏幕时,RecyclerView 不会销毁其视图。相反,RecyclerView 会对屏幕上滚动的新列表项重用该视图。这种重用可以显著提高性能,改善应用响应能力并降低功耗。

# 关键类

将多个不同的类搭配使用,可构建动态列表。

  • RecyclerView (opens new window) 是包含与您的数据对应的视图的 ViewGroup (opens new window)。它本身就是视图,因此,将 RecyclerView 添加到布局中的方式与添加任何其他界面元素相同。
  • 列表中的每个独立元素都由一个 ViewHolder 对象进行定义。创建 ViewHolder 时,它并没有任何关联的数据。创建 ViewHolder 后,RecyclerView 会将其绑定到其数据。您可以通过扩展 RecyclerView.ViewHolder (opens new window) 来定义 ViewHolder。
  • RecyclerView 会请求这些视图,并通过在 Adapter 中调用方法,将视图绑定到其数据。您可以通过扩展 RecyclerView.Adapter (opens new window) 来定义 Adapter。
  • 布局管理器负责排列列表中的各个元素。您可以使用 RecyclerView 库提供的某个布局管理器,也可以定义自己的布局管理器。布局管理器均基于库的 LayoutManager (opens new window) 抽象类。

可以在 RecyclerView 示例应用 (Kotlin) (opens new window)RecyclerView 示例应用 (Java) (opens new window) 中查看各部分如何组合在一起。

# 实现的步骤

如果您打算使用 RecyclerView,那么您需要完成几项工作。下面几部分对这些工作进行了详细介绍。

  1. 首先,确定列表或网格的外观。一般来说,您可以使用 RecyclerView 库的某个标准布局管理器。
  2. 设计列表中每个元素的外观和行为。根据此设计,扩展 ViewHolder 类。您的 ViewHolder 版本提供了列表项的所有功能。您的 ViewHolder 是 View 的封装容器,且该视图由 RecyclerView 管理。
  3. 定义用于将您的数据与 ViewHolder 视图相关联的 Adapter

还可以参考官方文档的高级自定义选项 (opens new window),根据自己的具体需求定制 RecyclerView。

# 规划布局

RecyclerView 中的列表项由 LayoutManager (opens new window) 类负责排列。RecyclerView 库提供了三种布局管理器,用于处理最常见的布局情况:

  • LinearLayoutManager (opens new window) 将各个项排列在一维列表中。
  • GridLayoutManager 将所有项排列在二维网格中:
    • 如果网格垂直排列,GridLayoutManager 会尽量使每行中所有元素的宽度和高度相同,但不同的行可以有不同的高度。
    • 如果网格水平排列,GridLayoutManager 会尽量使每列中所有元素的宽度和高度相同,但不同的列可以有不同的宽度。
  • StaggeredGridLayoutManager (opens new window)GridLayoutManager 类似,但不要求同一行中的列表项具有相同的高度(垂直网格有此要求)或同一列中的列表项具有相同的宽度(水平网格有此要求)。其结果是,同一行或同一列中的列表项可能会错落不齐。

# 实现 Adapter 和 ViewHolder

确定布局后,您需要实现 AdapterViewHolder。这两个类配合使用,共同定义数据的显示方式。ViewHolder 是包含列表中各列表项的布局的 View封装容器

Adapter 会根据需要创建 ViewHolder 对象,还会为这些视图设置数据。将视图与其数据相关联的过程称为**“绑定”**。

定义 Adapter 时,需要实现三个关键方法:

  • onCreateViewHolder():每当 RecyclerView 需要创建新的 ViewHolder 时,它都会调用此方法。此方法会创建并初始化 ViewHolder 及其关联的 View,但不会填充视图的内容,因为 ViewHolder 此时尚未绑定到具体数据。
  • onBindViewHolder()RecyclerView 调用此方法将 ViewHolder 与数据相关联。此方法会提取适当的数据,并使用该数据填充 ViewHolder 的布局。例如,如果 RecyclerView 显示的是一个名称列表,该方法可能会在列表中查找适当的名称,并填充 ViewHolder 的 TextView (opens new window)RecyclerView 调用此方法将 ViewHolder 与数据相关联。此方法会提取适当的数据,并使用该数据填充 ViewHolder 的布局。例如,如果 RecyclerView 显示的是一个名称列表,该方法可能会在列表中查找适当的名称,并填充 ViewHolder 的 TextView (opens new window) widget。
  • getItemCount():RecyclerView 调用此方法来获取数据集的大小。例如,在通讯簿应用中,这可能是地址总数。RecyclerView 使用此方法来确定什么时候没有更多的列表项可以显示。

# 简单示例

实现一个简单 RecyclerView 示例,创建一个 RecyclerView.Adapter 并包含一个显示数据列表的嵌套 ViewHolder。在本示例中,RecyclerView 显示了一个简单的文本+图片组合元素的列表。

  1. 定义列表项布局:layout_recycler_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        app:layout_constraintBottom_toBottomOf="@id/iv_icon"
        app:layout_constraintStart_toEndOf="@id/iv_icon"
        app:layout_constraintTop_toTopOf="@id/iv_icon" />
</androidx.constraintlayout.widget.ConstraintLayout>
  1. 定义布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_demo"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>
  1. 示例代码
public class DemoActivity extends AppCompatActivity {
    private static final String TAG = "DemoActivity";
    private static final int ICON_SPRING = 0;
    private static final int ICON_SUMMER = 1;
    private static final int ICON_AUTUMN = 2;
    private static final int ICON_WINTER = 3;
    private List<DataBean> mData = new ArrayList<>();
    private RecyclerView mRvDemo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo);

        // 初始化数据
        for (int i = 0; i < 16; i++) {
            int count = mData.size();
            int iconType = count % 4;
            int iconRes;
            String title;
            String tips;
            switch (iconType) {
                case ICON_SPRING:
                    iconRes = R.drawable.icon_1;
                    break;
                case ICON_SUMMER:
                    iconRes = R.drawable.icon_2;
                    break;
                case ICON_AUTUMN:
                    iconRes = R.drawable.icon_3;
                    break;
                case ICON_WINTER:
                    iconRes = R.drawable.icon_4;
                    break;
                default:
                    iconRes = 0;
            }
            title = "标题" + i;
            DataBean dataBean = new DataBean(iconRes, title);
            mData.add(dataBean);
        }

        mRvDemo = findViewById(R.id.rv_demo);
        MyAdapter myAdapter = new MyAdapter(this, mData);
        // 设置布局管理器为线性布局管理器
        mRvDemo.setLayoutManager(new LinearLayoutManager(this));
        // 设置适配器
        mRvDemo.setAdapter(myAdapter);
    }

    /**
     * 定义一个RecyclerView的适配器.
     */
    public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
        private Context mContext;
        private List<DataBean> mDataBeans;

        public MyAdapter(Context context, List<DataBean> dataBeans) {
            this.mContext = context;
            this.mDataBeans = dataBeans;
        }

        @NonNull
        @Override
        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            // 加载布局
            View view = LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.layout_recycler_item, parent, false);
            // 创建ViewHolder
            return new ViewHolder(view);
        }

        @Override
        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
            // 获取对应的数据对象
            DataBean dataBean = mDataBeans.get(position);
            // 通过ViewHolder的视图引用,将数据设置到对应的控件中
            holder.getIvIcon().setBackgroundResource(dataBean.getIcon());
            holder.getTvContent().setText(dataBean.getTitle());
        }

        @Override
        public int getItemCount() {
            return mDataBeans == null ? 0 : mDataBeans.size();
        }

        /**
         * 定义正在使用的视图类型的视图引用的ViewHolder.
         */
        public class ViewHolder extends RecyclerView.ViewHolder {
            private ImageView ivIcon;
            private TextView tvContent;

            public ViewHolder(View view) {
                super(view);
                ivIcon = view.findViewById(R.id.iv_icon);
                tvContent = view.findViewById(R.id.tv_content);
            }

            public TextView getTvContent() {
                return tvContent;
            }

            public ImageView getIvIcon() {
                return ivIcon;
            }
        }

    }

    /**
     * 定义列表数据对象.
     */
    public class DataBean {
        // 图标
        private int icon;
        // 标题
        private String title;

        public DataBean(int icon, String title) {
            this.icon = icon;
            this.title = title;
        }

        public int getIcon() {
            return icon;
        }

        public void setIcon(int icon) {
            this.icon = icon;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }
    }
}
  1. 效果图:

RecyclerView 基本使用示例效果图

# 自定义RecyclerView

# 布局管理器

RecyclerView 使用布局管理器将各个项放置在屏幕上,并确定何时重复使用不再对用户可见的项目视图。为了重复使用(或循环使用)视图,布局管理器可能会让 Adapter 使用数据集中的不同元素替换视图的内容。以这种方式循环使用视图可避免创建不必要的视图或执行代价高昂的 findViewById() 查找,从而提高性能。Android 支持库包含三个标准布局管理器,每个管理器都提供了许多自定义选项:

  • LinearLayoutManager 将各个项排列在一维列表中。将 RecyclerViewLinearLayoutManager 搭配使用可提供类似于 ListView 控件布局的功能。
  • GridLayoutManager 将各个项排列在二维网格中,就像棋盘上的方格一样。将 RecyclerViewGridLayoutManager 搭配使用可提供类似于 GridView 控件布局的功能。
  • StaggeredGridLayoutManager 将各个项排列在二维网格中,每一列都在前一列基础上稍微偏移,就像美国国旗中的星星一样。

如果这些布局管理器都不符合您的需求,您可以通过扩展 RecyclerView.LayoutManager 抽象类来自定义创建自己的布局管理器。

# 列表项添加动画

每当某个项发生变化时,RecyclerView 都会使用 animator 来更改其外观。该 animator 是一个扩展抽象 RecyclerView.ItemAnimator 类的对象。默认情况下,RecyclerView 使用 DefaultItemAnimator 来提供动画。如果您想提供自定义动画,可以通过扩展 RecyclerView.ItemAnimator 来定义自己的 animator 对象。

# 启用列表项选择

借助 recyclerview-selection (opens new window) 库,用户可以通过触摸或鼠标输入来选择 RecyclerView 列表中的项。您仍然可以控制所选项的视觉呈现效果。您也仍然可以控制用于约束选择行为的政策,例如符合入选条件的项以及可以选择的项数。

如需为 RecyclerView 实例添加对选择操作的支持,请按以下步骤操作:

  1. 确定要使用的选择键类型,然后构建 ItemKeyProvider。 有三种键类型可供您标识所选项:Parcelable(以及所有子类,如 Uri)、StringLong。如需详细了解选择键类型,请参阅 SelectionTracker.Builder (opens new window)

  2. 实现 ItemDetailsLookup (opens new window)ItemDetailsLookup (opens new window) 使选择功能库能够访问给定 MotionEvent 对应的 RecyclerView 项的相关信息。它实际上是由 RecyclerView.ViewHolder 实例支持(或从中提取)的 ItemDetails (opens new window) 实例的工厂。

  3. 更新 RecyclerView 中的 Views 项,以反映用户已将其选中或取消选中。 选择功能库不会为所选项提供默认视觉装饰。您必须在实现 onBindViewHolder() 时提供此设置。建议采用如下方法:
    - 在 onBindViewHolder() 中,对 View 对象调用 setActivated()(而不是 setSelected())并传入 truefalse(具体取决于对应的项否处于选中状态)。 - 更新视图样式以表示已激活状态。我们建议您使用颜色状态列表资源 (opens new window)来配置样式。

  4. 使用 ActionMode 为用户提供对所选项执行操作所需的工具。 ​注册 SelectionTracker.SelectionObserver (opens new window) 以在选择状态有变时接收通知。首次选择时,请启动 ActionMode 以向用户表示这一点,并提供特定于该选择的操作。例如,您可以向 ActionMode 栏添加删除按钮,然后将栏上的返回箭头连接到取消选择的操作。当选择列表变空时(如果用户取消选择最后一项),请不要忘记终止操作模式。

  5. 执行任何经过解释的次级操作 在事件处理流水线的最后,库可能会判断用户试图通过点按某个项来激活它或试图拖放某个项或一组选定项。请通过注册适当的监听器来回应这些解释。如需了解详情,请参阅 SelectionTracker.Builder (opens new window)

  6. 使用 SelectionTracker.Builder (opens new window) 汇编所有内容

    以下示例演示了如何使用 Long 选择键将这些部分组合在一起:

    SelectionTracker tracker = new SelectionTracker.Builder<>(
            "my-selection-id",
            recyclerView,
            new StableIdKeyProvider(recyclerView),
            new MyDetailsLookup(recyclerView),
            StorageStrategy.createLongStorage())
            .withOnItemActivatedListener(myItemActivatedListener)
            .build();
    

    为了构建 SelectionTracker (opens new window) 实例,您的应用必须向 SelectionTracker.Builder (opens new window) 提供您之前初始化 RecyclerView 时所用的同一个 RecyclerView.Adapter。因此,创建 RecyclerView.Adapter 后,您很可能需要在 SelectionTracker (opens new window) 实例一经创建后就将其注入到 RecyclerView.Adapter 中。 否则,您将无法通过 onBindViewHolder() (opens new window) 方法检查某个项的已选中状态。

  7. 将选中状态包含到 activity 生命周期 (opens new window)事件中。

    为了在不同的 activity 生命周期 (opens new window)事件之间保留选中状态,您的应用必须分别从 activity 的 onSaveInstanceState()onRestoreInstanceState() 方法调用选择状态跟踪器的 onSaveInstanceState() (opens new window)onRestoreInstanceState() (opens new window) 方法。您的应用还必须向 SelectionTracker.Builder (opens new window) 构造函数提供唯一的选择 ID。此 ID 是必需的,因为 activity 或 fragment 可能具有多个不同的可选择列表,而所有这些列表都需要保持其已保存的状态。

# 结束

本节介绍了 Android 中 RecyclerView 的常用属性以及相关使用方法、自定义实现的布局和列表项动画、列表项选择的方法,更多属性和用法请参考:RecyclerView (opens new window)