前言
为了解决 ListView 存在的拓展性差、需要手动优化性能等问题,Android 提供了滚动组件 RecycleView。本篇博客用于梳理 RecycleView 的使用方法。
RecycleView 的优点
- RecycleView 仅会处理当前现实在屏幕上的项。假如列表中有 1000个元素,而页面只显示其中 10 个,那么 RecycleView 仅处理这 10 个项
- 当某个项滚出屏幕时,RecycleView 会回收其视图。这个项被回收,用于填充新进入屏幕的内容。
- 当某一项发生变化时,仅重新绘制变化的那一项。
使用方法
添加依赖
新建一个 UiWidgetTest 文件
在 build.gradle (Moudule) 的 dependencies 加入以下内容,最新的版本可在 Recyclerview | Android Developers 中查看。
implementation("androidx.recyclerview:recyclerview:1.2.1")
implementation("androidx.recyclerview:recyclerview-selection:1.1.0")
加入代码后,点击 Sync Now 重新同步 gradle。
定制 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 文件夹下
创建实体类
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>
代码里做了以下几项更改:
- LinearLayout 的宽度由 80 dp 改为 match_parent 。因为瀑布流的宽度由布局的列数来自动适配,而不是一个固定值。
- 给 LinearLayout 设置了 5 dp 的外边距,使得每个子项之间看起来不会那么拥挤。
- 将 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){
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 嵌套混淆问题。
val position = viewHolder.bindingAdapterPosition
val position = viewHolder.absoluteAdapterPosition
|