详解 RecyclerView:从基础到布局与点击事件

前言

ListView 是之前 Android 主要使用的列表控件,因为其简单性,目前还有很多程序在使用 ListView。不过我们需要借助一些手段提升它的运行效率,否则性能非常差。并且 ListView 的扩展性也不好,它只能实现纵向滚动,想要实现横向滚动的话,是做不到的。

因此,Android 提供了一个更为强大和灵活的滚动控件:RecyclerView。它可以轻松实现和 ListView 同样的纵向列表效果,并且在性能优化、布局灵活方面更好。官方也推荐使用 RecyclerView,现在,我们来学习它的用法。

准备工作: 创建名为 RecyclerViewTest 的 Empty Views Activity 项目。

基本用法

为了让 RecyclerView 控件能够兼容所有安卓版本并且独立更新,Google 把 RecyclerView 控件定义在了 AndroidX 库里面,所以,我们在使用它之前,得先添加它的依赖。

build.gradle.kts(:app) 配置文件中,添加如下内容:

kotlin 复制代码
dependencies {
    implementation("androidx.recyclerview:recyclerview:1.4.0")
    // ... 其他依赖
}

接下来在 activity_main.xml 布局文件中添加 RecyclerView 控件:

xml 复制代码
<?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:orientation="vertical">


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

</LinearLayout>

在上述代码中,我们为 RecyclerView 控件指定了id,并且它的宽度和高度都设置为了 match_parent,这样能占满父布局。并且由于 RecyclerView 并不内置在系统 SDK 中,它是AndroidX 库的组件,所以要使用其完整包名。

为了实现和之前 ListView 示例同样的效果,我们将 ListViewTest 项目中的图片资源、Fruit 实体类以及 fruit_item.xml 列表项布局文件一并拷贝到当前项目中。

接下来,为 RecyclerView 添加适配器。新建 FruitAdapter Kotlin 类,让它继承自 RecyclerView.Adapter,并且泛型指定为 FruitAdapter.ViewHolder。其中 ViewHolder 是我们在 FruitAdapter 中自定义的内部类,代码如下所示:

kotlin 复制代码
class FruitAdapter(private val fruitList: List<Fruit>) :
    RecyclerView.Adapter<FruitAdapter.ViewHolder>() {

    // ViewHolder 持有列表项布局对应的 ItemFruitBinding 视图绑定类的实例
    inner class ViewHolder(private val binding: FruitItemBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(fruit: Fruit) {
            binding.fruitName.text = fruit.name
            binding.fruitImage.setImageResource(fruit.imageId)
        }
    }


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // 创建视图绑定对象
        val binding = FruitItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)

        // 将绑定对象传递给 ViewHolder
        return ViewHolder(binding)
    }

    override fun getItemCount(): Int {
        return fruitList.size
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val currentFruit = fruitList[position]
        
        // 自定义 bind 方法内部通过绑定对象直接访问视图实例,并设置数据
        holder.bind(currentFruit)
    }
}

其中 FruitItemBinding 是由 fruit_item.xml 生成的视图绑定类,具体可看官方文档

这是 RecyclerView 适配器的一种标准写法,比起 ListView 的适配器来说,逻辑更清晰。

首先我们定义了一个内部类 ViewHolder,它需要继承自 RecyclerView.ViewHolder 类。然后在 ViewHolder 的主构造函数中,我们传入了当前列表项布局对应的视图绑定类实例(FruitItemBinding),这样,ViewHolder 就能通过这个 FruitItemBinding 实例来直接访问列表项布局中的各个控件实例,并将列表项数据设置到这些控件中。

FruitAdapter 的主构造函数将要展示的数据源传递进来,由于 FruitAdapter 继承自 RecyclerView.Adapter,所以我们必须重写 onCreateViewHolder()onBindViewHolder()getItemCount() 这三个核心方法:

  • onCreateViewHolder() 方法是用于创建 ViewHolder 实例的,这个方法会在 RecyclerView 需要创建新的 ViewHolder 实例时被调用。我们在这个方法中加载了列表项的布局并创建 FruitItemBinding 实例,然后创建了一个 ViewHolder 实例并返回。

  • onBindViewHolder() 方法用于对列表项的数据进行赋值,这个方法会在每个列表项被滚动到屏幕内时被调用。在这里,我们根据 position 获取当前列表项的 Fruit 实例,然后调用自定义的 ViewHolder.bind() 方法,将数据设置到列表项布局中的 ImageView 和 TextView 控件上。

  • getItemCount() 方法用于得到数据源中列表项的数量,这里直接返回 fruitList 的长度。

适配器创建完成后,就可以在 MainActivity 中使用了。代码如下:

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    private val fruitList = ArrayList<Fruit>()
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        initFruits() // 初始化水果数据
        
        // 创建 LinearLayoutManager 对象,用于指定 RecyclerView 的布局方式
        val layoutManager = LinearLayoutManager(this)
        // LinearLayoutManager 默认是垂直方向
        binding.recyclerView.layoutManager = layoutManager

        // 创建 FruitAdapter 适配器实例
        val adapter = FruitAdapter(fruitList)
        // 将适配器设置给 RecyclerView
        binding.recyclerView.adapter = adapter
    }

    private fun initFruits() {
        repeat(2) {
            fruitList.add(Fruit("Apple", R.drawable.apple_pic))
            fruitList.add(Fruit("Banana", R.drawable.banana_pic))
            fruitList.add(Fruit("Orange", R.drawable.orange_pic))
            fruitList.add(Fruit("Watermelon", R.drawable.watermelon_pic))
            fruitList.add(Fruit("Pear", R.drawable.pear_pic))
            fruitList.add(Fruit("Grape", R.drawable.grape_pic))
            fruitList.add(Fruit("Pineapple", R.drawable.pineapple_pic))
            fruitList.add(Fruit("Strawberry", R.drawable.strawberry_pic))
            fruitList.add(Fruit("Cherry", R.drawable.cherry_pic))
            fruitList.add(Fruit("Mango", R.drawable.mango_pic))
        }
    }
}

我们使用了 initFruits() 方法来初始化了一些水果数据。在 onCreate 方法中,首先创建了一个 LinearLayoutManager 对象,并将它设置到了 RecyclerView 实例当中。LayoutManager 负责列表项在 RecyclerView 中的排列方式,而 LinearLayoutManager 则是表示列表项以线性方向(水平或垂直)排列。 LinearLayoutManager 表示线性布局。

接下来创建了 FruitAdapter 适配器实例,并且也设置到了 RecyclerView 实例当中,这样 RecyclerView 就和数据源之间建立连接了,可以展示列表了。

运行结果:

可以看到,使用 RecyclerView 实现的垂直列表效果和 ListView 几乎一样。当然这只是 RecyclerView 的基本用法而已,它的强大之处远不止于此,我们再来看看 RecyclerView 还能实现哪些功能。

实现横向滚动和瀑布流布局

横向滚动

RecyclerView 能实现横向滚动吗?

答案是可以的,并且还很简单。

首先我们修改列表项的布局 fruit_item.xml,将列表项的元素改为垂直排列,并为列表项设置一个固定的宽度,以适应横向滚动。

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="80dp"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/fruitImage"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="10dp" />

    <TextView
        android:id="@+id/fruitName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="10dp" />

</LinearLayout>

可以看到,我们将 LinearLayout 的方向改为了 vertical,并且宽度设置为了 80dp 固定值,为了保证横向滚动时每个列表项的宽度一致。因为水果名称有长有短,所以不能使用 wrap_content,这样会导致列表项有长有短,影响美观和滑动体验。

接下来在 MainActivity 中,修改 LinearLayoutManager 的方向:

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    private val fruitList = ArrayList<Fruit>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        initFruits() // 初始化水果数据

        val layoutManager = LinearLayoutManager(this)
        layoutManager.orientation = LinearLayoutManager.HORIZONTAL // 设置为水平排列
        binding.recyclerView.layoutManager = layoutManager

        val adapter = FruitAdapter(fruitList)
        binding.recyclerView.adapter = adapter
    }

    ...
}

我们调用了 LinearLayoutManagersetOrientation() 方法来修改布局的排列方向,由默认的纵向排列改为横向排列,这样 RecyclerView 就可以进行横向滚动了。

运行效果:

你可以用手指左右滑动来查看屏幕外的数据。

为什么 ListView 很难实现这种仅仅是改变了滚动方向的效果呢?这是因为 ListView 的布局排列由控件内部管理,是固定的。而 RecyclerView 将布局排列的工作交给了 LayoutManager,LayoutManager 制定了一套可扩展的布局排列接口,只要其实现类遵循接口规范进行实现,就能定制出不同排列方式的布局,从而实现高度的灵活性。

瀑布流布局

除了之前的 LinearLayoutManager 线性布局外,RecyclerView 还提供了 GridLayoutManager 和 StaggeredGridLayoutManager 这两种内置的布局排列方式。

GridLayoutManager 可以用于实现网格布局,而 StaggeredGridLayoutManager 可用于实现瀑布流布局(交错网格布局)。我们现在就来实现一个瀑布流布局吧。

首先,还是修改列表项的布局 fruit_item.xml

xml 复制代码
<?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="wrap_content"
    android:layout_margin="5dp"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/fruitImage"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="10dp" />

    <TextView
        android:id="@+id/fruitName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:layout_marginTop="10dp" />

</LinearLayout>

这里,我们将 LinearLayout 的宽度设为了 match_parent,因为在瀑布流布局中,每个列表项的实际宽度会根据布局的列数动态变化,不是固定值。我们还将 TextView 的对齐属性改成了左对齐。

然后在 MainActivity 中修改LayoutManager 的类型:

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    private val fruitList = ArrayList<Fruit>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        initFruits() // 初始化水果数据

        // 瀑布流布局排列方式
        val layoutManager = StaggeredGridLayoutManager(
            3,                                     // 指定列数
            StaggeredGridLayoutManager.VERTICAL    // 指定排列方向为垂直
        )
        binding.recyclerView.layoutManager = layoutManager

        val adapter = FruitAdapter(fruitList)
        binding.recyclerView.adapter = adapter
    }

    private fun initFruits() {
        repeat(2) {
            fruitList.add(Fruit(getRandomLengthString("Apple"), R.drawable.apple_pic))
            fruitList.add(Fruit(getRandomLengthString("Banana"), R.drawable.banana_pic))
            fruitList.add(Fruit(getRandomLengthString("Orange"), R.drawable.orange_pic))
            fruitList.add(Fruit(getRandomLengthString("Watermelon"), R.drawable.watermelon_pic))
            fruitList.add(Fruit(getRandomLengthString("Pear"), R.drawable.pear_pic))
            fruitList.add(Fruit(getRandomLengthString("Grape"), R.drawable.grape_pic))
            fruitList.add(Fruit(getRandomLengthString("Pineapple"), R.drawable.pineapple_pic))
            fruitList.add(Fruit(getRandomLengthString("Strawberry"), R.drawable.strawberry_pic))
            fruitList.add(Fruit(getRandomLengthString("Cherry"), R.drawable.cherry_pic))
            fruitList.add(Fruit(getRandomLengthString("Mango"), R.drawable.mango_pic))
        }
    }

    // 获取随机重复次数的字符串,以模拟不同高度的文本内容
    private fun getRandomLengthString(str: String): String {
        val n = (1..20).random()
        return StringBuilder().run {
            repeat(n) {
                append(str)
            }
            toString()
        }
    }
}

我们在 onCreate() 方法中创建了 StaggeredGridLayoutManager 实例,它的构造函数接收两个参数:

  • 第一个参数用于指定布局的列数,这里为 3 列。

  • 第二个参数用于指定布局的排列方向,这里让布局纵向(垂直)排列。

然后,把创建好的 StaggeredGridLayoutManager 实例设置到 RecyclerView 实例当中就可以了。

为了使瀑布流的效果更加明显,列表项的高度应该不同。所以我们通过 getRandomLengthString 方法来随机修改每个列表项中水果名称的长度,由于 TextView 的高度是 wrap_content,这将导致列表项整体高度不同。

运行效果:

RecyclerView的点击事件

和 ListView 一样,RecyclerView 也可以响应点击事件,否则不就是花瓶了吗?

不过它没有像 ListView 提供类似于 setOnItemClickListener() 这样的方法来为每一个列表项统一注册点击监听器。我们只能手动给每一个列表项注册点击事件。

为什么其他地方 RecyclerView 处处优于 ListView,在点击事件上却反而不如 ListView 呢?

其实这并不是 RecyclerView 的设计缺陷,反而提高了灵活性。因为我们往往想要为列表项中的某个控件注册点击事件时,而 ListView 实现起来就非常麻烦了。所以 RecyclerView 干脆舍去了为每一个列表项统一注册点击的监听器,将所有的点击事件交由各个具体的 View 视图去处理,使我们可以精细地控制每个控件的行为。

我们在 FruitAdapteronCreateViewHolder() 方法中为列表项的根视图或是其内部的其它视图注册点击事件:

kotlin 复制代码
class FruitAdapter(private val fruitList: List<Fruit>) :
    RecyclerView.Adapter<FruitAdapter.ViewHolder>() {

    ...

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // 创建视图绑定对象
        val binding = FruitItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)

        // 将绑定对象传递给 ViewHolder
        val viewHolder = ViewHolder(binding)

        // 为整个列表项(根视图)注册点击事件
        binding.root.setOnClickListener {
            // 获取 ViewHolder 在适配器中的位置
            val position = viewHolder.bindingAdapterPosition
            // 检查 position 是否有效
            if (position != RecyclerView.NO_POSITION) {
                val fruit = fruitList[position]
                Toast.makeText(parent.context, "You clicked view: ${fruit.name}", Toast.LENGTH_SHORT).show()
            }
        }

        // 为列表项中的 ImageView 注册点击事件
        binding.fruitImage.setOnClickListener {
            val position = viewHolder.bindingAdapterPosition // 获取 ViewHolder 在适配器中的位置
            // 检查 position 是否有效
            if (position != RecyclerView.NO_POSITION) {
                val fruit = fruitList[position]
                Toast.makeText(parent.context, "You clicked image: ${fruit.name}", Toast.LENGTH_SHORT).show()
            }
        }

        return viewHolder
    }

    ...
}

可以看到,在 onCreateViewHolder() 方法中,我们分别为列表项的最外层布局和水果图片 ImageView 都注册了注册点击事件。RecyclerView 可以轻松实现列表项中任意控件或布局的点击事件。

然后在点击事件的回调中,我们通过 viewHolder.bindingAdapterPosition 获取了用户当前点击的列表项在适配器中的位置,注意:使用position之前,要判断它是否等于RecyclerView.NO_POSITION,避免列表项因被移除或是位置发生变化所导致的数组越界异常, 然后拿到对应的 Fruit 实例,使用 Toast 来显示提示信息。

重新运行程序,效果:

可以看到点击水果图片(如苹果)部分时,会显示"you clicked image AppleAppleAppleApple"的提示。点击了列表项中图片以外的区域(如樱桃的文字部分)时,会显示"you clicked image CherryCherryCherryCherryCherryCherryCherryCherry"的提示,因为点击事件会被列表项最外层布局捕获。

相关推荐
Dnelic-5 小时前
Android 5G NR 状态类型介绍
android·5g·telephony·connectivity·自学笔记·移动网络数据
吗喽对你问好6 小时前
Android UI 控件详解实践
android·ui
东风西巷9 小时前
X-plore File Manager v4.34.02 修改版:安卓设备上的全能文件管理器
android·网络·软件需求
yzpyzp10 小时前
Android 15中的16KB大页有何优势?
android
安卓开发者10 小时前
Android Room 持久化库:简化数据库操作
android·数据库
程序视点10 小时前
FadCam安卓后台录制神器:2025最全使用指南(开源/免费/息屏录制)
android
猿小蔡11 小时前
Android ADB命令之内存统计与分析
android
游戏开发爱好者812 小时前
没有 Mac,如何上架 iOS App?多项目复用与流程标准化实战分享
android·ios·小程序·https·uni-app·iphone·webview
你过来啊你12 小时前
Android开发中nfc协议分析
android
Auspemak-Derafru13 小时前
安卓上的迷之K_1171477665
android