之前需要实现一个效果,横向滑动,中间是被选中状态,item更大,其他item小,左右滑动时,动态变大变小,且每个item间距一样。
开发非常痛苦,想了很多方案,网上也没找到合适的。经过艰苦粪斗粪发涂墙,终于勉强弄出来了。只能说,有时候,人还是不要指望依赖外部,自己才最可靠。
先分享下我的有道笔记:https://note.youdao.com/s/KnYcmg4t
这里不再诉苦了,分享干货。
使用RecyclerView.LayoutManager 来布局
class MyLayoutManager: RecyclerView.LayoutManager
之后布局,要使item能左右滑动,类似wheel ,第一个和最后一个可滑动到中间。需要重写LayoutManager的方法
override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {
// 获取重新布局前,滑动的位置,用于刷新后位置与之前一致。
val lastPosition = if (childCount == 0) Int.MAX_VALUE else getDecoratedLeft(getChildAt(0)!!)
// 回收旧的childItem
recycler?.also { removeAndRecycleAllViews(it) }
// 如果当前没有childItem,中断
if (state == null || state.itemCount == 0) {
return
}
// 启动布局,我们假设每个item尺寸都一样
recycler?.also {
var length = 0 // 展示完后childItem总的宽度
var measureStart = 0 // 第一个childItem居中时,滑动的位置
var measureEnd = 0 // 末位childItem居中时,滑动的位置
// 计算位置和尺寸
for (i in 0 until itemCount) {
val view = recycler.getViewForPosition(i) // 获取itemChild
addView(view) // 添加到RecyclerView
measureChildWithMargins(view, 0, 0) // 核算尺寸
// 获取childItem宽度和ItemDecoration尺寸的总宽度
val vWidth = getDecoratedMeasuredWidth(view)
length += vWidth // 尺寸叠加
if (i == 0) measureStart = (width - view.width) / 2
if (i == itemCount - 1) measureEnd = (width + view.width) / 2 - length
}
// 根据上次展示位置,计算当前展示childItem的坐标
var offsetX =
if (lastPosition > measureStart) measureStart
else if (lastPosition < measureEnd) measureEnd
else lastPosition
for (i in 0 until itemCount) {
val view = getChildAt(i)!!
val vWidth = getDecoratedMeasuredWidth(view)
val vHeight = getDecoratedMeasuredHeight(view)
layoutDecorated(view, offsetX, (height - vHeight).shr(1),
vWidth + offsetX, (height + vHeight).shr(1))
offsetX += vWidth
// 此处是自定义方法,根据childView的位置,修改其展示方式
setChildScaleAndTranslate(view)
}
}
}
需要实现左右滑动,需要重写如下方法:
override fun canScrollHorizontally(): Boolean {
return true
}
其次,需要重写指定方法,实现itemChild的左右移动
override fun scrollHorizontallyBy(dx: Int,
recycler: RecyclerView.Recycler?,
state: RecyclerView.State?): Int {
if (itemCount == 0) {
offsetChildrenHorizontal(0)
return 0
}
var move = -dx // 此次滑动距离
var result = dx
val center = width.shr(1) // 组件X轴中间位置坐标
if (recycler != null) {
if (dx < 0) { // 向左滑动
val first = getChildAt(0)!!
val vCenter = first.let { it.right + it.left shr 1 }
if (vCenter - dx > center) {
move = center - vCenter
result = 0
}
} else if (dx > 0) { // 向右滑动
val last = getChildAt(itemCount - 1)!!
val vCenter = last.let { it.right + it.left shr 1 }
if (vCenter - dx < center) {
move = center - vCenter
result = 0
}
}
}
// 调用父类方法实现具体滑动
offsetChildrenHorizontal(move)
for (i in 0 until childCount) {
val it = getChildAt(i)!!
// 此处是自定义方法,根据childView的位置,修改其展示方式
setChildScaleAndTranslate(it, i == 1)
}
isScrollingByUser = state != null // 用来判断是否是用户手动操作
return result
}
具体偏差和缩放展示计算
scaleRate是个常量,0~1之间(不含),缩放后的图片的比率
/**
* 根据view的位置去修改其scale和translate属性
* @param it childView对象
*/
private fun setChildScaleAndTranslate(it: View) {
val center = width.shr(1)
val tWidth = it.width + getLeftDecorationWidth(it) +
getRightDecorationWidth(it)
val distance = (getDecoratedRight(it)
+ getDecoratedLeft(it)).shr(1) - center
val distanceAbs = abs(distance)
val rate =
if (distanceAbs < tWidth)
(1f - (1 - scaleRate) * (distanceAbs.toFloat() / tWidth))
else scaleRate
it.scaleX = rate
it.scaleY = rate
val halfOffset = (1f - scaleRate) * tWidth / 2
val number = distanceAbs / tWidth
val r = (distanceAbs % tWidth).toFloat() / tWidth
val translateX =
if (distance == 0) 0f
else if (number == 0 && distance > 0) ((1 - scaleRate) / 2 * it.width - getLeftDecorationWidth(it)) * r
else if (number == 0) ((1 - scaleRate) / 2 * it.width - getRightDecorationWidth(it)) * r
else if (distance > 0) (distanceAbs.toFloat() / tWidth * 2 - 1) * halfOffset - number * getLeftDecorationWidth(it) * r
else (distanceAbs.toFloat() / tWidth * 2 - 1) * halfOffset - number * getRightDecorationWidth(it) - getLeftDecorationWidth(it) * r
it.translationX = if (distance > 0) -translateX else translateX
实现Edge效果,需要重写如下方法
/** 当前滑动偏差,实际返回的最左侧childView.left*/
override fun computeHorizontalScrollOffset(state: RecyclerView.State):Int {if (childCount == 0) return 0for (i in 0 until childCount)
getChildAt(i)?.also {
if (it.left > 0) return it.left
}
return super.computeHorizontalScrollOffset(state)}
/** 设置可视的范围*/
override fun computeHorizontalScrollExtent(state: RecyclerView.State):Int {
return if (childCount == 0) return 0 else width
}
/**组件可滑动范围, 没有childView时为0,否则,是最右侧childView.right - 最左侧childView.left */
override fun computeHorizontalScrollRange(state: RecyclerView.State): Int {
return if (childCount == 0) 0
else {
val startChild = getChildAt(0)!!
val endChild = getChildAt(childCount - 1)!!
endChild.right - startChild.left
}
}
RecyclerView被清理时,记得回收childView
override fun onDetachedFromWindow(view: RecyclerView?, recycler: RecyclerView.Recycler?) {
recycler?.also { removeAndRecycleAllViews(it) }
}
要重写默认布局参数
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
return RecyclerView.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
要重写scrollToPosition等方法,确保外部调用能准确滚动到指定item上
override fun scrollToPosition(position: Int) {
if (position < 0 || itemCount == 0 || position >= itemCount) return
val v = getChildAt(position) ?: return
val move = (v.right + v.left).shr(1) - width.shr(1)
selectListener?.onSelect(position, false)
for (i in 0 until itemCount) {
if (i == position) {
v.isSelected = true
continue
}
getChildAt(i)?.also { if (it.isSelected) it.isSelected = false }
}
smoothAnimBy(move)
}
override fun smoothScrollToPosition(recyclerView: RecyclerView?
, state: RecyclerView.State?, position: Int) {
if (position < 0 || itemCount == 0 || position >= itemCount)
return
val v = getChildAt(position) ?: return
val move = (v.right + v.left).shr(1) - width.shr(1)
selectListener?.onSelect(position, false)
for (i in 0 until itemCount) {
if (i == position) {
v.isSelected = true
continue
}
getChildAt(i)?.also { if (it.isSelected) it.isSelected = false }
}
smoothAnimBy(move, 100L * (
abs(move) / (getDecoratedRight(v) + getDecoratedLeft(v)) + 1))
}
剩下滑动停止后,item自动滚动到中间对齐。
需要计算距离和动画,结合ValueAnimator实现, 并且滑动后选中监听反馈。
重写onScrollStateChanged方法,监听列表滚动停止时(RecyclerView.SCROLL_STATE_IDLE),计算需要动画滚动多少距离。
// 定义一个动画对象
private var smoothScroller: ValueAnimator? = null
// 加速器
private val mInterpolator = DecelerateInterpolator()
// 用于保存动画偏差的全局变量
private var lastAnimValue: Int = 0
// 动画监听回调
private val smoothScrollerListener by lazy {
ValueAnimator.AnimatorUpdateListener { animation ->
animation?.also {
val value = (it.animatedValue as Int)
scrollHorizontallyBy(value - lastAnimValue, null, null)
lastAnimValue = value
}
}
}
/**
* 动画滑动
* @param end 偏移量
* @param duration 动画时间,默认200ms
*/
private fun smoothAnimBy(end: Int, duration: Long = 200) {
smoothScroller?.also {
it.removeUpdateListener(smoothScrollerListener)
it.cancel()
}
lastAnimValue = 0
if (end == 0) return
smoothScroller = ValueAnimator.ofInt(0, end).apply {
this.duration = duration
interpolator = mInterpolator
addUpdateListener(smoothScrollerListener)
start()
}
}
// 监听回调对象
var selectListener: MySelectListener? = null
// 回调接口
interface MySelectListener {
fun onSelect(position: Int, isSelectByUser: Boolean = false)
}
// 重写方法,接收滚动时,启动动画滚动到正确位置
override fun onScrollStateChanged(state: Int) {
if (state == RecyclerView.SCROLL_STATE_IDLE) {
val center = width.shr(1)
for (i in 0 until itemCount) {
val v = getChildAt(i)!!
val marginLeft = getLeftDecorationWidth(v)
val marginRight = getRightDecorationWidth(v)
v.isSelected = center <= v.right + marginRight
&& center >= v.left - marginLeft // 选中状态
if (v.isSelected) {
val move = (v.right + v.left).shr(1) - center
selectListener?.onSelect(i, isScrollingByUser)
isScrollingByUser = false
smoothAnimBy(move)
}
}
}
}