全部代码:github
方式有二
- 组合控件,RecyclerView + View
- 自定义RecyclerView
高亮的动画在1中性能只需要控制View
,2中需要重绘RecyclerView,1性能好一点,但也无伤大雅。
才100来行代码,性能优异。
主要步骤
- 添加ItemDecoration使第一个和最后一个Item可以滚动到屏幕中央
- 添加SnapHelper重写方法
- 重写OnDrawForeGround绘制高亮区域
- 添加OnScrollListener当滑动停止可以根据当前Item高度重绘高亮区域
1. 添加ItemDecoration使第一个和最后一个Item可以滚动到屏幕中央
很简单
判断是第一个/最后一个Item,顶部/底部添加距离
val decoration = object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: State
) {
super.getItemOffsets(outRect, view, parent, state)
this@HighLightRecyclerView.adapter?.let {
if (parent.getChildAdapterPosition(view) == 0)
outRect.top = (parent.measuredHeight - view.layoutParams.height).shr(1)
else if (parent.getChildAdapterPosition(view) == it.itemCount - 1)
outRect.bottom = (parent.measuredHeight - view.layoutParams.height).shr(1)
}
}
}
2. RecyclerView
添加SnapHelper
并重写findSnapView()
方法
思路就是,根据停止时各个Item的位置判断RecyclerView应该对其哪个Item
无非就是一个工具类的两个比较特别的方法
OrientationHelper
的getDecoratedStart(v:View)
有两种情况,字面意思是“获取Item距离顶部位置”。
- Item设置了Margin或Decoration这些偏移量,注意是
Start
,也就是一般情况下的Left
或者Top
,视Orientation
而定。那么返回值为Item顶部距RecyclerView顶部距离 - 偏移量
- 没有设置偏移量,返回值为
Item高度
相应地,
OrientationHelper
的getDecoratedMeasurement(v:View)
也有两种情况。字面意思是“获取Item所占空间大小”。
- 有偏移,返回
偏移+高度
- 无偏移,返回
高度
高亮Item的需求因为涉及到第一步中偏移的设置,主要有三种情况
- 顶部
Start
为0,Measurement
为顶部偏移+高度
- 中部
Start
为顶部距离,Measurement
为Item高度
- 底部
Start
为顶部距离,Measurement
为底部偏移+高度
这地方不弄情况特别乱,我列出表格大概是这个样子:
位置 | getDecoratedStart | getDecoratedMeasurement |
---|---|---|
顶部 | 0 | OffsetTop + Height |
中部 | TopDistance | Height |
底部 | TopDistance | OffsetBottom + Height |
我们让Item居中,它应该是 顶部距离+ItemHeight/2 == RecyclerView.height/2
伪代码就是 TopDistance+Height/2 = Parent.Height/2
尝试用一行代码写,发现没办法,只能分情况
也就是
pos为0 helper.getDecoratedStart(it) + helper.getDecoratedMeasurement(it) - it.layoutParams.height
其余情况为 helper.getDecoratedStart(it) + it.layoutParams.height.shr(1)
想明白了也就那么回事。
代码如下:
override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
return when {
layoutManager == null -> null
layoutManager.childCount == 0 -> null
else -> {
var closestChild: View? = null
var absClosest = Int.MAX_VALUE
val helper = OrientationHelper.createVerticalHelper(layoutManager)
val center = helper.startAfterPadding + helper.totalSpace.shr(1)
var childCenter: Int
var distance: Int
for (i in 0 until layoutManager.childCount) {
layoutManager.getChildAt(i)?.let {
childCenter = if (i == 0)
helper.getDecoratedStart(it) + helper.getDecoratedMeasurement(it) - it.layoutParams.height
else helper.getDecoratedStart(it) + it.layoutParams.height.shr(1)
distance = abs(center - childCenter)
if (distance < absClosest) {
absClosest = distance
closestChild = it
}
}
}
closestChild
}
}
}
3. 重写OnDrawForeGround绘制高亮区域
path.addRect(0f, 0f, width.toFloat(), height.toFloat(), Path.Direction.CW)
path.addRect(
0f,
(height - highLightHeight).shr(1).toFloat(),
width.toFloat(),
(height + highLightHeight).shr(1).toFloat(),
Path.Direction.CCW
)
没啥好讲的,主要就是Path.Direction
要讲讲
这个高亮区域,有两个矩形组成,第一个是画全屏的遮罩,第二个是从第一个矩形中抠出这个区域,反方向与Android绘制定义有关,记住它能抠出形状就行。就不用涉及PorterDuff
类的相关概念了。
4. 添加OnScrollListener当滑动停止可以根据当前Item高度重绘高亮区域
啊,其实这个如果不要动画,就很简单,一行代码
highLightHeight = height
但是,要动画也是一行代码
if (newState == SCROLL_STATE_IDLE) {
snapHelper.findSnapView(layoutManager)?.let {
animator = ValueAnimator.ofInt(highLightHeight, it.layoutParams.height)
.setDuration(300)
animator.removeAllUpdateListeners()
animator.addUpdateListener { va ->
highLightHeight = va.animatedValue as Int
}
animator.start()
}
}
网友评论