RecyclerView实战
基础概念
RecyclerView是用于高效展示大数据集列表网格的核心组件,相比传统的ListView和GridView,它通过视图回收机制和灵活的布局管理显著提升性能。
ListView 在处理大量数据时会面临性能瓶颈,因为它每次滚动时都会重新加载所有的视图。而 RecyclerView 则通过 ViewHolder 进行视图复用,不仅能显著提高性能,还支持多种布局管理器(LayoutManager)和动画效果。
核心要素
RecyclerView 的实现主要依赖于以下三个核心组件:
- Adapter:Adapter 是数据源与 RecyclerView 之间的桥梁,负责将数据绑定到视图上。
- ViewHolder:ViewHolder 是对 RecyclerView 中每个条目视图的缓存,它避免了每次滑动时都要重新查找视图的性能问题。
- LayoutManager:LayoutManager 决定了 RecyclerView 中条目的排列方式,它支持多种布局类型,如 LinearLayoutManager(线性布局)、GridLayoutManager(网格布局)、StaggeredGridLayoutManager(瀑布流布局)等。
其使用方式分为四步走:
- 创建子母布局
- 创建Adapter
- 创建ViewHolder
- 关联到RecyclerView并设置LayoutManager
实现示例
基础实现
第一步:创建子母布局
母布局指的是RecyclerView本身所在的布局,子布局是指里面具体的Item的布局。
本身的布局:
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fadingEdge="none"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
- 一般只需要指定id、layout_width、layout_height即可
- fadingEdge的作用是在什么时候启用渐变效果,可选项有none、any、horizontal和vertical。
- layoutManager 如果不在xml中指定则一定要在Activity 中设置,否则无法显示出视图
Item 布局:略,如果想显示多类型条目则要创建多个 Item 布局。在实际开发中,我们经常需要在 RecyclerView 中显示不同类型的条目,比如文本项、图片项,或是带有图片的文本项。
第二步:创建Adapter
这里以双类型条目作为演示,单条目类型要更加简单
class RecyclerViewDemoAdapter(private val items: List<Any>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
// 创建 ViewHolder 的方法,根据条目类型加载不同的布局
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
// 如果条目类型是 TYPE_TEXT,加载文本条目的布局
TYPE_TEXT -> TextViewHolder(parent)
// 如果条目类型是 TYPE_IMAGE_TEXT,加载混合条目的布局
TYPE_IMAGE_TEXT -> ImageTextViewHolder(parent)
// 如果条目类型无效,抛出异常
else -> throw IllegalArgumentException("Invalid view type")
}
}
// 绑定数据到 ViewHolder
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
// 根据当前条目的类型,进行相应的数据绑定
when (getItemViewType(position)) {
TYPE_TEXT -> (holder as TextViewHolder).bind(items[position] as String)
TYPE_IMAGE_TEXT -> (holder as ImageTextViewHolder).bind(items[position] as Pair<Int, String>)
}
}
// 返回列表项的数量
override fun getItemCount(): Int = items.size
// 根据当前位置返回条目的类型
override fun getItemViewType(position: Int): Int {
return when (items[position]) {
// 如果是字符串类型,返回文本条目类型
is String -> TYPE_TEXT
// 如果是 Pair 类型(包含图片和文本),返回图片在左,文本在右条目类型
is Pair<*, *> -> TYPE_IMAGE_TEXT
// 如果条目类型无效,抛出异常
else -> throw IllegalArgumentException("Invalid item type")
}
}
}
① 数据传入:可以通过构造函数将数据从 Activity 那边传递过来,一般会是一个 List。在示例中,构造函数接收一个 List<Any>
类型的参数 items,表示适配器管理的数据集 ② onCreateViewHolder 方法:用于创建 ViewHolder 对象,它是 RecyclerView 中条目的容器,根据不同的 viewType 加载不同的布局文件,创建相应的 ViewHolder。若是单条目类型直接返回使用的 ViewHolder 即可。 ③ onBindViewHolder 方法:用于将数据绑定到 ViewHolder 上。根据 ViewHolder 的类型和当前条目的位置,将数据集中的数据赋值给 ViewHolder 中的视图。 ④ getItemCount 方法:返回数据集的大小,告诉 RecyclerView 要显示的条目总数。示例中直接返回 items.size。 ⑤ getItemViewType 方法:可选,如果是单条目则不需要。返回值决定 onCreateViewHolder 创建哪种类型的 ViewHolder。
至于 ViewHolder 对应的是那个视图则在 ViewHolder 的具体实现中设置。
第三步:创建ViewHolder
class TextViewHolder(
parent: ViewGroup,
private val binding: ItemTextBinding = ItemTextBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
) : RecyclerView.ViewHolder(binding.root) {
fun bind(text: String) {
binding.header.text = text
}
}
// 混合条目的 ViewHolder,负责绑定图片资源和文本数据
class ImageTextViewHolder(
parent: ViewGroup,
private val binding: ItemImageTextBinding = ItemImageTextBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Pair<Int, String>) {
binding.icon.setImageResource(item.first)
binding.textView.text = item.second
}
}
注意:我们在 ViewHolder 的构造函数里面指定了默认的 binding,因此在 Adapter 里面调用 ViewHolder 只需要传入 parent 参数即可,不需要再明确指定 binding。
第四步:关联到RecyclerView并设置LayoutManager
在 Activity 中:
private lateinit var recyclerView: RecyclerView // RV
private lateinit var adapter: RecyclerViewDemoAdapter // Adapter
private val itemList = mutableListOf<Any>(...) // 数据
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
recyclerView = binding.recyclerView //设置 RV
adapter = RecyclerViewDemoAdapter(itemList) // 设置 Adapter
recyclerView.adapter = adapter // 绑定 RV 和 Adapter
recyclerView.layoutManager = LinearLayoutManager(this)//设置 RV 的 layoutManager
}
注意:layoutManager 一定要设置,除非你在 xml 中已经指定了。
使用 BRVAH 简化
BRVAH(BaseRecyclerViewAdapterHelper)是一个用于简化开源框架
class AnimationAdapter() : BaseQuickAdapter<String, QuickViewHolder>() {
override fun onCreateViewHolder(
context: Context,
parent: ViewGroup,
viewType: Int
): QuickViewHolder {
return QuickViewHolder(R.layout.item_text, parent)
}
override fun onBindViewHolder(
holder: QuickViewHolder,
position: Int,
item: String?
) {
holder.setText(R.id.header, item)
holder.getView<TextView>(R.id.header).setOnClickListener {
PopTip.show("点击了 $item")
}
}
}
使用BaseQuickAdapter的注意点:
- 如果 ViewHolder 很简单,可以直接使用 QuickViewHolder 并指定视图
- 不需要 getItemCount 方法
- 传入数据可以使用参数传递,也可以使用 submitList 方法
- BaseQuickAdapter 泛型参数的第一个参数是 具体的数据类型 而不是 List
添加动画效果
BaseQuickAdapter内置了很多的动画效果,使用方式也很简单。例如我们有class AnimationAdapter() : BaseQuickAdapter,那么:
private val mAdapter: AnimationAdapter = AnimationAdapter().apply {
// 打开 Adapter 的动画
animationEnable = true
// 是否是首次显示时候加载动画
isAnimationFirstOnly = false
}
// 设置动画效果
mAdapter.setItemAnimation(BaseQuickAdapter.AnimationType.AlphaIn)
可选择的动画效果有:
- AlphaIn
- ScaleIn
- SlideInBottom
- SlideInLeft
- SlideInRight
例如:ScaleIn 的动画如下:
添加 Header 和 Footer
第一步:使用 QuickAdapterHelper 类来管理 Adapter
private lateinit var recyclerView: RecyclerView
private val mAdapter: AnimationAdapter = AnimationAdapter()
private val helper by lazy(LazyThreadSafetyMode.NONE) {
QuickAdapterHelper.Builder(mAdapter)
.build()
}
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
recyclerView = binding.recyclerView //设置 RV
adapter = RecyclerViewDemoAdapter(itemList) // 设置 Adapter
recyclerView.adapter = helper.adapter
recyclerView.layoutManager = LinearLayoutManager(this)//设置 RV 的 layoutManager
}
第二步:使用 addBeforeAdapter 和 addAfterAdapter 方法来添加 Header 和 Footer
private fun addHeader() {
helper.addBeforeAdapter(0, HeaderAdapter())
}
private fun addFooter() {
helper.addAfterAdapter(FooterAdapter())
}
第三步:实现 HeaderAdapter 和 FooterAdapter 这两个 Adapter 分别绑定 Header 和 Footer 的视图
// HeaderAdapter
class HeaderAdapter : BaseSingleItemAdapter<Any, HeaderAdapter.VH>() {
class VH(view: View) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.simple_list_item, parent, false)
return VH(view)
}
override fun onBindViewHolder(holder: VH, item: Any?) { }
}
// FooterAdapter
class FooterAdapter(
) : BaseSingleItemAdapter<Any, QuickViewHolder>() {
override fun onCreateViewHolder(
context: Context,
parent: ViewGroup,
viewType: Int
): QuickViewHolder {
return QuickViewHolder(R.layout.top_view, parent)
}
override fun onBindViewHolder(holder: QuickViewHolder, item: Any?) { }
}
如果视图比较简单可以直接使用 BaseSingleItemAdapter
加载和空状态
加载状态:
空视图状态:
第一步:打开空布局功能
mAdapter.isStateViewEnable = true
第二步:设计加载状态的动画
val waitDialog = WaitDialog.show("正在加载...")
.setOnBackPressedListener {
PopTip.show("操作进行中...")
.setButton("取消等待") { _, _ ->
WaitDialog.dismiss()
false
}
false
}
这一步可以是转圈的Dialog弹窗或是其他的什么,我这里使用WaitDialog,当然也可以和第三步一样使用一个自定义的View 视图。
第三步:根据不同的状态来显示不同的内容
private var status = false
private val errorView: View
get() {
val errorView = layoutInflater.inflate(R.layout.error_view, FrameLayout(this), false)
errorView.setOnClickListener { onRefresh() }
return errorView
}
recyclerView.postDelayed({
waitDialog.doDismiss()
if (status) { // 模拟网络错误
// 方式二:传入View
mAdapter.submitList(null)
mAdapter.stateView = errorView
} else {
mAdapter.submitList(DataServer.getStringItems5())
}
status = !status
}, 1000)
首先需要一个 view 类型来当做空状态的视图,然后在合适的时候设置 stateView 来改变视图。
刷新和加载更多
顶部下拉刷新
底部上滑加载
这里指的是在rv最顶部下拉刷新,在底部上滑加载更多数据。 这两个功能需要借助QuickAdapterHelper来实现,还需要借助SwipeRefreshLayout控件。
第一步:在xml中使用SwipeRefreshLayout包裹rv
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white80"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fadingEdge="none"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
第二步:下拉刷新 - 设置刷新箭头的颜色与触发的方法
private fun initRefreshLayout() {
binding.refreshLayout.setColorSchemeColors(Color.rgb(47, 223, 189))
binding.refreshLayout.setOnRefreshListener { refresh() }
}
在refresh()中请求数据并且重新设置数据,在我们下拉的时候会出现转圈的箭头表示正在加载数据,而箭头的消失需要手动设置:
// 假设这里刷新完了数据
binding.refreshLayout.isRefreshing = false // 取消加载箭头
第三步:加载更多 - 在 helper 上设置
private var helper: QuickAdapterHelper = QuickAdapterHelper.Builder(mAdapter)
.setTrailingLoadStateAdapter(object : OnTrailingListener {
override fun onLoad() {
// 加载数据的方法
request()
}
override fun onFailRetry() {
request()
}
override fun isAllowLoading(): Boolean {
// 在下拉刷新的时候不要触发加载更多
return !binding.refreshLayout.isRefreshing
}
}).build()
当我们滑动到最底部的时候,会触发onLoad()方法,这时候可以加载更多数据。
加载更多数据的时候一般都是分页加载。因此需要判断情况,如果是第一次加载可以使用mAdapter.submitList(data)来设置数据,后续加载可以使用mAdapter.addAll(data)来添加数据。如果网络错误可能需要弹窗提示,当数据加载完毕没有更多数据的时候也需要提示。
第四步:加载更多的状态设置
helper.trailingLoadState = LoadState.None
trailingLoadState 有多种状态:
- LoadState.None:当前没有任何状态(例如:初始化时,刷新数据时)
- LoadState.NotLoading(true):当前未加载,并且当前未发生错误,并且已达到分页尾。此时 rv 底端会显示
no more data
- LoadState.NotLoading(false):当前未加载,并且当前未发生错误,并且未达到分页尾。此时 rv 底端会自动触发
onLoad()
方法 - LoadState.Error(e): 出现错误,此时 rv 底端会显示出现错误,点击重试的英文
拖动和滑动
拖动改变位置:
滑动删除条目:
第一步:在BaseQuickAdapter中完善方法
首先需要使Adapter继承自DragAndSwipeDataCallback,然后实现方法:
override fun dataMove(fromPosition: Int, toPosition: Int) {
move(fromPosition, toPosition)
}
override fun dataRemoveAt(position: Int) {
removeAt(position)
}
第二步:设置滑动事件和拖动事件的监听(可选)
private val quickDragAndSwipe = QuickDragAndSwipe()
.setDragMoveFlags(
ItemTouchHelper.UP or ItemTouchHelper.DOWN or
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
)
.setSwipeMoveFlags(ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT)
quickDragAndSwipe.attachToRecyclerView(recyclerView)
.setDataCallback(mAdapter)
.setItemDragListener(listener) // OnItemDragListener
.setItemSwipeListener(swipeListener) // OnItemSwipeListener
listener 的示例:
// 拖拽监听
val listener: OnItemDragListener = object : OnItemDragListener {
override fun onItemDragStart(viewHolder: RecyclerView.ViewHolder?, pos: Int) {
// 震动反馈
vibrate()
LogUtils.d("drag start")
val holder = viewHolder as QuickViewHolder? ?: return
// 开始时,item背景色变化,demo这里使用了一个动画渐变,使得自然
val startColor = Color.WHITE
val endColor = Color.rgb(245, 245, 245)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val v = ValueAnimator.ofArgb(startColor, endColor)
v.addUpdateListener { animation: ValueAnimator ->
holder.itemView.setBackgroundColor(
animation.animatedValue as Int
)
}
v.duration = 300
v.start()
}
}
override fun onItemDragMoving(
source: RecyclerView.ViewHolder,
from: Int,
target: RecyclerView.ViewHolder,
to: Int
) {
LogUtils.d(
"move from: " + source.bindingAdapterPosition + " to: " + target.bindingAdapterPosition
)
}
override fun onItemDragEnd(viewHolder: RecyclerView.ViewHolder, pos: Int) {
LogUtils.d("drag end")
val holder = viewHolder as QuickViewHolder
// 结束时,item背景色变化,demo这里使用了一个动画渐变,使得自然
val startColor = Color.rgb(245, 245, 245)
val endColor = Color.WHITE
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val v = ValueAnimator.ofArgb(startColor, endColor)
v.addUpdateListener { animation: ValueAnimator ->
holder.itemView.setBackgroundColor(
animation.animatedValue as Int
)
}
v.duration = 300
v.start()
}
}
}
// 滑动监听
val swipeListener: OnItemSwipeListener = object : OnItemSwipeListener {
override fun onItemSwipeStart(
viewHolder: RecyclerView.ViewHolder?,
bindingAdapterPosition: Int
) {
LogUtils.d("onItemSwipeStart")
}
override fun onItemSwipeEnd(
viewHolder: RecyclerView.ViewHolder,
bindingAdapterPosition: Int
) {
LogUtils.d("onItemSwipeEnd")
}
override fun onItemSwiped(
viewHolder: RecyclerView.ViewHolder,
direction: Int,
bindingAdapterPosition: Int
) {
LogUtils.d("onItemSwiped")
}
override fun onItemSwipeMoving(
canvas: Canvas,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
isCurrentlyActive: Boolean
) {
LogUtils.d("onItemSwipeMoving")
}
}