本文主要讲述RecyclerView和ListView的区别,ItemDecoration实现分割线,边距和背景,以及SnapHelper的使用。
一、RecyclerView和ListView
1. 性能和视图重用
- ListView 使用的是
ViewHolder
模式来实现视图的重用,但需要手动配置,容易出错。 - RecyclerView 内置了视图重用机制,且提供了
ViewHolder
作为标准。RecyclerView
更高效,尤其是在大量数据滚动时,减少了内存开销。
2. 布局管理(LayoutManager)
- ListView 只支持垂直列表展示,没有内置的灵活布局功能。
- RecyclerView 支持多种布局管理器 (
LinearLayoutManager
、GridLayoutManager
、StaggeredGridLayoutManager
等),可以轻松实现垂直、水平列表、网格和瀑布流布局。此外,你可以自定义LayoutManager
实现特殊布局。
3. 动画效果
- ListView 没有内置动画,添加/删除数据时不会自动提供过渡动画。
- RecyclerView 提供默认的添加、删除、移动和更改的动画效果,简化了过渡动画的实现,也可自定义动画。
4. 数据更新
- ListView 需要调用
notifyDataSetChanged()
更新数据,不能精确更新具体位置的数据,导致性能损失。 - RecyclerView 可以使用
notifyItemInserted()
、notifyItemRemoved()
等方法,只更新特定项,更加高效。同时,通过ListAdapter
和DiffUtil
可实现数据的精确对比和最小范围更新。
5. 灵活性和扩展性
- ListView 功能比较有限,适用于简单的列表显示,难以扩展或实现复杂的布局。
- RecyclerView 提供了灵活的接口和抽象,可以轻松实现拖动、滑动删除等高级功能,适合复杂的界面需求。
7. 优缺点
- ListView 实现简单,但缺乏灵活性,尤其在复杂应用场景中,显得力不从心。
- RecyclerView 需要更多的代码,尤其是在实现简单的列表展示时,可能显得繁琐。对于非常简单的需求,
RecyclerView
的配置和管理复杂度较高。
总结
RecyclerView
在性能和扩展性方面明显优于 ListView
,适合现代应用的需求。对于简单的列表或无需频繁数据更新的场景,ListView
也可以考虑,但通常推荐使用 RecyclerView
来替代 ListView
以获得更好的用户体验。
二、RecyclerView ItemDecoration
1. 实现分割线
使用 DividerItemDecoration
类为 RecyclerView
添加分割线:
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)// 使用系统提供的 DividerItemDecoration
val dividerItemDecoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
recyclerView.addItemDecoration(dividerItemDecoration)
实现效果:
如果想要自定义分割线,可以继承 RecyclerView.ItemDecoration
并重写 onDraw()
方法:
class CustomDividerDecoration(private val dividerHeight: Int, private val dividerColor: Int) : RecyclerView.ItemDecoration() {private val paint = Paint()init {paint.color = dividerColorpaint.style = Paint.Style.FILL}override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {val left = parent.paddingLeftval right = parent.width - parent.paddingRightfor (i in 0 until parent.childCount - 1) {val child = parent.getChildAt(i)val params = child.layoutParams as RecyclerView.LayoutParamsval top = child.bottom + params.bottomMarginval bottom = top + dividerHeightcanvas.drawRect(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat(), paint)}}
}
使用:
val customDivider = CustomDividerDecoration(10, Color.GRAY)
recyclerView.addItemDecoration(customDivider)
效果:
tips:
DividerItemDecoration
默认会在每个RecyclerView
子项的底部绘制分割线,垂直布局的RecyclerView
会在子项的下方画分割线,而水平布局的RecyclerView
会在子项的右侧画分割线。如果你需要自定义分割线的位置,比如只在顶部、左右侧或者每个子项的周围,都可以通过自定义
ItemDecoration
并手动绘制来实现。
2. 实现边距 (Margin)
创建一个 ItemDecoration
子类为每个子项设置边距:
class MarginItemDecoration(private val space: Int) : RecyclerView.ItemDecoration() {override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {outRect.left = spaceoutRect.right = spaceoutRect.top = spaceoutRect.bottom = space}
}
使用:
val marginDecoration = MarginItemDecoration(16) // 设置边距大小
recyclerView.addItemDecoration(marginDecoration)
实现效果
tips:为了方便观看,我给每个子项增加了一个浅黄色的背景, 可以看到,他的上下左右都有16像素的间距,如果你的上下间距为6px,分割线恰好为12px,那么间距的高度就刚好和分割线一致,就不会看到中间的白色间隔了。
3. 实现背景
如果需要为每个子项添加背景色,可以创建一个自定义 ItemDecoration
:
class BackgroundItemDecoration(private val backgroundColor: Int) : RecyclerView.ItemDecoration() {private val paint = Paint()init {paint.color = backgroundColorpaint.style = Paint.Style.FILL}override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {for (i in 0 until parent.childCount) {val child = parent.getChildAt(i)val left = child.left.toFloat()val top = child.top.toFloat()val right = child.right.toFloat()val bottom = child.bottom.toFloat()canvas.drawRect(left, top, right, bottom, paint)}}
}
使用:
val backgroundDecoration = BackgroundItemDecoration(Color.RED)
recyclerView.addItemDecoration(backgroundDecoration)
效果:
因为我给item设置了浅黄色背景,所以可以看到我的列表,只有header因为没有设置背景,所以才是红色的。
如果我给xml设置的背景色有透明度,或者直接不设置背景,那么就会生效。
效果:item背景色设置为有透明度的颜色 android:background="#5AFFFFCC"
可以看到,背景生效了,header的背景是纯红色,下面数据列表item的背景则是橙红色。
4. 总结
因为分开写例子会更清楚些,我就分开实现的,实际需求肯定比这个繁琐,混合搭配更合适
- 分割线:可以使用
DividerItemDecoration
或自定义的ItemDecoration
。- 边距:通过自定义
ItemDecoration
的getItemOffsets()
方法来设置Rect
的间距。- 背景:使用
onDraw()
在每个子项的区域绘制背景色。
三、RecyclerView SnapHelper
SnapHelper是一个辅助类,用于帮助 RecyclerView 在滚动停止时将某个 item 精确地对齐到 RecyclerView 的特定位置,例如对齐到 RecyclerView 的开头、中间或末尾。使用 SnapHelper 可以实现类似于分页滚动、对齐中心点的效果,适合用于Banner轮播图、分页卡片等效果。
Android 提供了两种内置的 SnapHelper
子类:
- LinearSnapHelper:让 RecyclerView 在滚动停止时将最接近中心的 item 对齐到 RecyclerView 的开头(或者视图中心)。
- PagerSnapHelper:实现类似 ViewPager 的分页滚动效果,让每次滚动只显示一个 item,自动将其对齐到开始位置。
1. 使用 LinearSnapHelper
例如,当我有一个横向的画廊的时候,使用viewpager,一次划一下展示下一张,不可以一下划多页,那么如果是一个列表去实现滑动,又很容易划到一半,两张图片都占据在屏幕中,会很丑,之前做过类似需求,去算item在屏幕中展示的偏移量让item滚动到正确位置,不稳定,哎,当时也没想到用SnapHelper
LinearSnapHelper 适用于横向或纵向的 RecyclerView
,用于对齐单个 item 到视图的中心。
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
recyclerView.adapter = YourAdapter()// 创建并附加 LinearSnapHelper
val snapHelper = LinearSnapHelper()
snapHelper.attachToRecyclerView(recyclerView)
实现效果:
可以看到,当我滑动到一定位置时,他就自动划到上一项,且上一项自己居中了。 如果手指快速滑动,则会一次性滚动多个Item。
当然,因为我上面的图片设置得有点大,所以一次就看到一张图,如果Item宽度小一点,则是这样的效果:
是不是和Banner展示得很像,且item始终会居中
2. 使用 PagerSnapHelper
PagerSnapHelper 的效果类似于 ViewPager,每次滚动只显示一个 item,非常适合类似分页效果的需求。因为之前有个画廊的需求,使用Viewpager过于繁琐,后面直接使用RecyclerView搭配PagerSnapHelper,很轻松就实现了。
代码:
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
recyclerView.adapter = YourAdapter()// 创建并附加 PagerSnapHelper
val pagerSnapHelper = PagerSnapHelper()
pagerSnapHelper.attachToRecyclerView(recyclerView)
PagerSnapHelper
将自动分页滚动。这个效果很像 ViewPager 但不需要单独的 ViewPager 控件。一次滑动只能滚动一个Item哦。
3. 监听 Snap 事件
为了实现更高级的需求,可以监听 Snap 事件。例如,当某个 item 滚动对齐时,可以触发回调。可以利用 RecyclerView.OnScrollListener
和 SnapHelper
来检测当前对齐的 item 索引:
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {super.onScrollStateChanged(recyclerView, newState)if (newState == RecyclerView.SCROLL_STATE_IDLE) {val snapView = snapHelper.findSnapView(recyclerView.layoutManager)val position = recyclerView.layoutManager?.getPosition(snapView ?: return)position?.let {// Do something with the snapped item positionLog.d("SnapHelper", "Snapped to position: $position")}}}
})
在这个监听器中,当滚动停止并对齐某个 item 时,会输出该 item 的位置。这在更新 UI 或加载相应的数据时非常有用。