前言

为了解决 ListView 存在的拓展性差、需要手动优化性能等问题,Android 提供了滚动组件 RecycleView。本篇博客用于梳理 RecycleView 的使用方法。

RecycleView 的优点

  1. RecycleView 仅会处理当前现实在屏幕上的项。假如列表中有 1000个元素,而页面只显示其中 10 个,那么 RecycleView 仅处理这 10 个项
  2. 当某个项滚出屏幕时,RecycleView 会回收其视图。这个项被回收,用于填充新进入屏幕的内容。
  3. 当某一项发生变化时,仅重新绘制变化的那一项。

使用方法

添加依赖

新建一个 UiWidgetTest 文件

在 build.gradle (Moudule) 的 dependencies 加入以下内容,最新的版本可在 Recyclerview | Android Developers 中查看。

implementation("androidx.recyclerview:recyclerview:1.2.1")
// For control over item selection of both touch and mouse driven selection
implementation("androidx.recyclerview:recyclerview-selection:1.1.0")

加入代码后,点击 Sync Now 重新同步 gradle。

Android RecycView flow布局 android recycleview_android

定制 RecycleView 界面

修改 activity_main.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">

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

在 layout 文件夹下新建一个 girl_item.xml 文件,这个文件用于展示 RecycleView 中的每个数据项。

<?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="60dp">

    <ImageView
        android:id="@+id/girlImage"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="10dp" />

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

</LinearLayout>

这个子布局很简单,就是一张图片加一个标签

准备图片资源

准备一些图片资源,放入 res -> drawable 文件夹下

Android RecycView flow布局 android recycleview_ide_02

创建实体类

RecycleView 中每一个 item 中的元素都来自于一个对象,即RecycleView 中的数据来自一个集合。

新建一个 Girl 类,代码如下所示。

package com.example.uiwidgettest

class Girl(val name: String,val imageId:Int)

imageId 为 Int 型,存储对应图片的资源 id。

适配器 Adapter

前面说到 RecycleView 中的数据来源是 List<Object>,但是数组并不能直接应用到 RecycleView 中。

假设数组是传统的 mirco usb 数据线,RecycleView 是一个 type-C 类型的手机。

Micro usb 数据线是无法在 type-C 类型手机上使用,所以需要一个转接头,把 mirco-usb 转成 type-C

Adapter 翻译成适配器,用于把 List 集合适配成 RecycleView 的可用类型。

新建一个 GirlAdapter 类 ,让它继承 RecycleView.Adapter 类,将范型制定为 GirlAdapter.ViewHolder

package com.example.uiwidgettest

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class GirlAdapter(private val girls: List<Girl>) : RecyclerView.Adapter<GirlAdapter.ViewHolder>() {

    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val girlName: TextView = view.findViewById(R.id.girlName)
        val girlImage: ImageView = view.findViewById(R.id.girlImage)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.girl_item, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val girl = girls[position]
        holder.girlName.text = girl.name
        holder.girlImage.setImageResource(girl.imageId)
    }

    override fun getItemCount() = girls.size
}

代码解析

  • GirlAdapter 参数为 Girl 集合,GirlAdapter 将此集合转化为 RecycleView 的可用类型
  • ViewHolder 翻译为 View 持有者,它用于描述一个子 View 中的数据,以及其在 RecycleView 的位置信息
  • 内部类 ViewHolder继承自 RecycleView.ViewHolder()。ViewHolder 需要一个非空的 View 对象作为参数
  • RecycleView.Adapter 是一个抽象类,继承该类需要实现 onCreateViewHolder(),onBindViewHolder() 和 getItemCount() 方法
  • onCreateViewHolder() 用于创建 ViewHolder,参数 view 是 子项 XML 对应的类。用布局加载器将 girl_item 加载成一个 View 类
  • onBindViewHolder() 用于对 RecycleView 的子项赋值,它在每个子项滚动到屏幕中时执行。根据 position 获取到对象实例,将数据设置到 ViewHolder 的元素中。
  • getItemCount() 用于告诉 RecycleView 一共有多少个子项。

使用 RecycleView

修改 MainActivity 中的代码

package com.example.uiwidgettest

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.uiwidgettest.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private val girls = ArrayList<Girl>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        initGirls()
        val layoutManager = LinearLayoutManager(this)
        binding.recycleView.layoutManager = layoutManager
        val girlAdapter = GirlAdapter(girls)
        binding.recycleView.adapter = girlAdapter
    }

    private fun initGirls() {
        repeat(3) {
            girls.add(Girl("Girl1", R.drawable.girl1))
            girls.add(Girl("Girl2", R.drawable.girl2))
            girls.add(Girl("Girl3", R.drawable.girl3))
            girls.add(Girl("Girl4", R.drawable.girl4))
            girls.add(Girl("Girl5", R.drawable.girl5))
        }
    }
}

代码中,先初始化了一个 girls 集合,然后在 onCreate() 方法中创建了一个 LinearLayoutManager对象,把它设置到 RecycleView 中。

layoutManager 用于设置 RecycleView 的布局方,LinearLayoutManager 表示线性布局。

创建一个 GirlAdapter 实例,传入 girls 集合到 GirlAdapter 的构造器中。

最后调用 RecycleView 的 setAdapter() 方法来完成适配器设置。

至此,运行 app 就能看到 RecycleView 的运行效果。

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

相比于 ListView ,RecycleView 的另一个优点就是它能很容易的实现横向滚动及修改布局方式。

这得益于 LayoutManager ,通过对 LayoutManager 的设置就能轻松修改 RecycleView 的布局和滚动方向。

实现横向滚动

修改 girl_item.xml

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

    <ImageView
        android:id="@+id/girlImage"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="10dp" />

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

</LinearLayout>

把 LinearLayout 改为垂直方向,宽度改为 80 dp,高度改为 “wrap_content”。

因为这里实现的是横向滚动,所以最好让他们的宽度保持一致,这样看起来比较美观。

两个子 View 的 layout_gravity 改为 “center_horizontal”。

在 MainActivity 中设置 layoutManager 的排列方向

layoutManager.orientation = LinearLayoutManager.HORIZONTAL

实现瀑布流布局

瀑布流布局,是一种多行等宽不等高元素实现的参差不齐的排列,如下图所示:


Android RecycView flow布局 android recycleview_ide_03

首先修改子项布局文件 girl_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="5dp">

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

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

</LinearLayout>

代码里做了以下几项更改:

  1. LinearLayout 的宽度由 80 dp 改为 match_parent 。因为瀑布流的宽度由布局的列数来自动适配,而不是一个固定值。
  2. 给 LinearLayout 设置了 5 dp 的外边距,使得每个子项之间看起来不会那么拥挤。
  3. 将 TextView 的对齐属性由居中改为居左。因为后面要通过改变文字的长度来使每个子元素不等高。
package com.example.uiwidgettest

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.example.uiwidgettest.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private val girls = ArrayList<Girl>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        initGirls()
        val layoutManager = StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL)
        binding.recycleView.layoutManager = layoutManager
        val girlAdapter = GirlAdapter(girls)
        binding.recycleView.adapter = girlAdapter
    }

    private fun initGirls() {
        repeat(5) {
            girls.add(Girl(getRandomName("Girl1"), R.drawable.girl1))
            girls.add(Girl(getRandomName("Girl2"), R.drawable.girl2))
            girls.add(Girl(getRandomName("Girl3"), R.drawable.girl3))
            girls.add(Girl(getRandomName("Girl4"), R.drawable.girl4))
            girls.add(Girl(getRandomName("Girl5"), R.drawable.girl5))
        }
    }

    private fun getRandomName(name: String): String {
        val n = (1..20).random()
        val builder = StringBuilder()
        repeat(n){
                builder.append(name)
        }
        return builder.toString()
    }
}

首先将 layoutManager 的布局方式由线性布局改为瀑布流布局

val layoutManager = StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL)

StaggeredGridLayoutManager 的第一个参数用于指定布局的列数,第二个参数用于指定布局的排列方向

其次,为了使不同元素拥有不同的高度,添加了一个 getRandomName() 方法。

private fun getRandomName(name: String): String {
        val n = (1..20).random() 	// 通过随机数的方式拓展名字的长度
        val builder = StringBuilder()
        repeat(n){ // 将原来的名字重复 n 次
                builder.append(name)
        }
        return builder.toString()
    }

RecycleView 的点击事件

修改适配器类 GirlAdapter 的代码

package com.example.uiwidgettest

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView

class GirlAdapter(private val girls: List<Girl>) : RecyclerView.Adapter<GirlAdapter.ViewHolder>() {

    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val girlName: TextView = view.findViewById(R.id.girlName)
        val girlImage: ImageView = view.findViewById(R.id.girlImage)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.girl_item, parent, false)
        val viewHolder = ViewHolder(view)
        viewHolder.girlName.setOnClickListener{
            val position = viewHolder.adapterPosition
            val girl = girls[position]
            Toast.makeText(parent.context,"你点击了文字 ${girl.name}$",Toast.LENGTH_SHORT).show()
        }
        viewHolder.girlImage.setOnClickListener{
            val position = viewHolder.adapterPosition
            val girl = girls[position]
            Toast.makeText(parent.context,"你点击了图片 ${girl.name}$",Toast.LENGTH_SHORT).show()
        }
        return viewHolder
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val girl = girls[position]
        holder.girlName.text = girl.name
        holder.girlImage.setImageResource(girl.imageId)
    }

    override fun getItemCount() = girls.size
}

在 onCreateViewHolder()方法中,对 viewholder 的子 view 注册点击事件。

viewholder.adapterPosition 用于获取该子项的位置,根据该位置就能操作数组中的对象

然而,该方法已经被废弃。因为当 Adapter 存在嵌套时,调用此方法会引发歧义

Google 提出了两个新方法 getBindingAdapterPosition() 和 getAbsoluteAdapterPosition() 来解决可能存在的

Adapter 嵌套混淆问题。

// 如果你希望访问的是 Adapter 中内容的位置 ,使用 bindingAdapterPosition
val position = viewHolder.bindingAdapterPosition
// 如果你希望访问的是 适配器相对于 RecycleView 的位置,使用 bindingAdapterPosition
val position = viewHolder.absoluteAdapterPosition