RecycleView的基本使用

前言

为了解决 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。

image-20220513195831301

定制 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 文件夹下

image-20220513203128085

创建实体类

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

实现瀑布流布局

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

瀑布流布局

首先修改子项布局文件 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
  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值