RecyclerVIew->加速再减速的RecyclerVIew平滑对齐工具类SnapHelper

发布于:2024-06-24 ⋅ 阅读:(65) ⋅ 点赞:(0)

XML文件

  • ItemViewXML文件R.layout.shape_item_view
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:background="@drawable/shape_item_view">
    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:textSize="16sp" />
</FrameLayout>
  • 滑动到对齐ItemViewXML文件 R.drawable.shape_item_view_selected
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#FFFFFF" />
    <corners android:radius="8dp" />
    <stroke android:color="#FFFF00" android:width="5dp" />
</shape>
  • 未滑动到对齐ItemViewXML文件 R.drawable.shape_item_view
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#FFFFFF" />
    <corners android:radius="8dp" />
    <stroke android:color="#000000" android:width="5dp" />
</shape>
  • Activity的XML文件R.layout.activity_main
<?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"
    android:gravity="center">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_marginLeft="@dimen/edit_crop_frame_padding"
        android:layout_marginRight="@dimen/edit_crop_frame_padding"
        android:orientation="horizontal" />
</LinearLayout>

RecyclerView代码

  • Adapter代码
class MyAdapter(private val numbers: List<Int>) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
	// 记录选中位置
    private var selectedPosition = RecyclerView.NO_POSITION

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_view, parent, false)
        return MyViewHolder(view)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.textView.text = numbers[position].toString()
        if (selectedPosition == position) {
            holder.itemView.setBackgroundResource(R.drawable.shape_item_view_selected)
        } else {
            holder.itemView.setBackgroundResource(R.drawable.shape_item_view)
        }
    }
    
    override fun getItemCount() = numbers.size
	
	// 给外部工具类SnapHelper实现类使用,滚动到对齐位置,修改ItemView的轮廓
    fun setSelectedPosition(position: Int) {
        val oldPosition = selectedPosition
        selectedPosition = position
        notifyItemChanged(oldPosition)
        notifyItemChanged(selectedPosition)
    }

    class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val textView: TextView = itemView.findViewById(R.id.textView)
    }
}
  • ItemDecoration代码
class SpaceItemDecoration(private val spaceSize: Int, private val itemSize : Int) : RecyclerView.ItemDecoration() {

    private val paint = Paint().apply {
        color = Color.RED
        style = Paint.Style.FILL
    }

    var spacePadding = 0
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        super.getItemOffsets(outRect, view, parent, state)
        // 正常Item的Decoration
        outRect.left = spaceSize
        outRect.right = spaceSize
        // 第一个和最后一个Item的Decoration
        spacePadding = (parent.measuredWidth / 2 - itemSize / 2)
        val size = parent.adapter?.itemCount ?: 0
        val position = parent.getChildAdapterPosition(view)
        if (position == 0) {
            outRect.left = spacePadding
        } else if (position == size - 1) {
            outRect.right = spacePadding
        }
    }

    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDraw(c, parent, state)
        val childCount = parent.childCount
        for (i in 0 until childCount) {
            val child = parent.getChildAt(i)
            val position = parent.getChildAdapterPosition(child)
            val params = child.layoutParams as RecyclerView.LayoutParams
            var left : Int
            var right : Int
            var top : Int
            var bottom : Int
            if (position == 0) {
                left = child.left - params.leftMargin - spacePadding
                right = child.right + params.rightMargin + spaceSize
                top = child.top - params.topMargin
                bottom = child.bottom + params.bottomMargin
            } else if (position == parent.adapter?.itemCount!! - 1) {
                left = child.left - params.leftMargin - spaceSize
                right = child.right + params.rightMargin + spacePadding
                top = child.top - params.topMargin
                bottom = child.bottom + params.bottomMargin
            } else {
                // 绘制其他 Item 的装饰
                left = child.left - params.leftMargin - spaceSize
                right = child.right + params.rightMargin + spaceSize
                top = child.top - params.topMargin
                bottom = child.bottom + params.bottomMargin
            }
            c.drawRect(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat(), paint)
        }
    }
}

RecyclerView对齐工具类SnapHelper实现类代码

  • attachToRecyclerView()方法:将RecyclerView对齐操作交给SnapHelper实现类
override fun attachToRecyclerView(recyclerView: RecyclerView?) {
    Log.i(TAG, "attachToRecyclerView")
    mRecyclerView = recyclerView
    super.attachToRecyclerView(recyclerView)
}
  • createScroller()方法:创建惯性滑动的Scroller
    • onTargetFound()回调方法:找到对齐位置之后回调,计算目标位置到对齐位置需要滚动的距离和时间
    • calculateSpeedPerPixel()回调方法:滚动一英寸所需时间除以屏幕密度,得到滚动一像素所需的时间
override fun createScroller(layoutManager: RecyclerView.LayoutManager?): RecyclerView.SmoothScroller? {
    if (layoutManager !is RecyclerView.SmoothScroller.ScrollVectorProvider) {
        return null
    }
    Log.i(TAG, "createScroller")
    return object : LinearSmoothScroller(mRecyclerView?.context) {
        override fun onTargetFound(targetView: View, state: RecyclerView.State, action: Action) {
            if (mRecyclerView == null) return
            Log.i(TAG, "onTargetFound")

            // 计算当前位置到目标位置的距离
            val snapDistances : IntArray? = calculateDistanceToFinalSnap(mRecyclerView?.layoutManager!!, targetView)
            val dx = snapDistances?.get(0) ?: 0
            val dy = snapDistances?.get(1) ?: 0
            // 这里增加滑动时间,可以使得滑动速度变慢
            val time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy))) * 10
            if (time > 0) {
                // 这里传入的是LinearOutSlowInInterpolator(), 也就是先加速再减速的插值器
                // 相比LinearSnapHelper中的DecelerateInterpolator, 这个插值器更符合自然滑动的效果
                action.update(dx, dy, time, mInterpolator)
            }
        }
        
        override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float {
            Log.i(TAG, "calculateSpeedPerPixel")
            // 计算滑动一个像素的时间
            return MILLISECONDS_PER_INCH / displayMetrics?.densityDpi!!
        }
    }
}
  • findTargetSnapPosition()方法:找到需要对齐的ItemView的位置
    • RecyclerView.SmoothScroller.ScrollVectorProvider.computeScrollVectorForPosition():计算从0的位置滚动到ItemCount-1的位置需要滚动的方向,vectorForEnd.x表示水平方向(>0向右,<0向左),vectorForEnd.y表示竖直方向(>0向下,<0向上),正负值由结束位置和开始位置的差值得出
override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager?, velocityX: Int, velocityY: Int): Int {
	// 判断layoutManager是否实现了RecyclerView.SmoothScroller.ScrollVectorProvider这个接口
	if (layoutManager !is RecyclerView.SmoothScroller.ScrollVectorProvider) return RecyclerView.NO_POSITION
	
	// 判断ItemView个数是否小于等于0
	val itemCount = layoutManager.itemCount
	if (itemCount == 0) return RecyclerView.NO_POSITION
	
	// 找到需要对齐的ItemView
	val currentView = findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION
	
	// 获取需要对齐ItemView的位置
	val currentPosition = layoutManager.getPosition(currentView)
	if (currentPosition == RecyclerView.NO_POSITION) return RecyclerView.NO_POSITION
	
	// 判断layoutManager的布局方向
	val vectorProvider = layoutManager as RecyclerView.SmoothScroller.ScrollVectorProvider
	val vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1) ?: return RecyclerView.NO_POSITION
	
	Log.i(TAG, "findTargetSnapPosition")
	// 计算水平, 垂直方向最多能惯性滑动的ItemView个数,在当前ItemView的位置上,进行加减Position操作
	var maxHorizontalItemViewCount: Int
	var maxVerticalItemViewCount: Int
	
	if (layoutManager.canScrollHorizontally()) {
	    maxHorizontalItemViewCount = estimateNextPositionDiffForFling(layoutManager, getHorizontalHelper(layoutManager), velocityX, 0)
	    var sign = Math.signum(velocityX.toFloat())
	    if (sign == 0f) sign = 1f
	    // 限制最多能惯性滑动的ItemView个数,限制滑动的 ItemView 的个数在 1 到 2 之间
	    maxHorizontalItemViewCount = (sign * Math.min(Math.max(Math.abs(maxHorizontalItemViewCount), 0), 2)).toInt()
	    if (vectorForEnd.x < 0) {
	        maxHorizontalItemViewCount = - maxHorizontalItemViewCount
	    }
	}else{
	    maxHorizontalItemViewCount = 0
	}
	
	if (layoutManager.canScrollVertically()) {
	    maxVerticalItemViewCount = estimateNextPositionDiffForFling(layoutManager, getVerticalHelper(layoutManager), 0, velocityY)
	    var sign = Math.signum(velocityY.toFloat())
	    if (sign == 0f) sign = 1f
	    // 限制最多能惯性滑动的ItemView个数,限制滑动的 ItemView 的个数在 1 到 2 之间
	    maxVerticalItemViewCount = (sign * Math.min(Math.max(Math.abs(maxVerticalItemViewCount), 0), 2)).toInt()
	    if (vectorForEnd.y < 0) {
	        maxVerticalItemViewCount = - maxVerticalItemViewCount
	    }
	}else{
	    maxVerticalItemViewCount = 0
	}
	
	// 根据滑动的方向,计算出最终的 ItemView 个数
	val finalItemCount = if(layoutManager.canScrollHorizontally()){
	    maxHorizontalItemViewCount
	}else{
	    maxVerticalItemViewCount
	}
	
	if (finalItemCount == 0) return RecyclerView.NO_POSITION
	
	// 确定最终的对齐位置,并做边界处理
	var targetPosition = currentPosition + finalItemCount
	if (targetPosition < 0) targetPosition = 0
	if (targetPosition >= layoutManager.itemCount) targetPosition = layoutManager.itemCount - 1
	return targetPosition
}
  • findSnapView()方法:调用findCenterView()找到最接近中心点的ItemView
    • findCenterView()方法:拿到每个ItemViewleft加上自身宽度的一半和RecyclerView的中心点进行比较,找到最接近中心点的ItemView
override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
	Log.i(TAG, "findSnapView")
	if (layoutManager!!.canScrollVertically()) {
	    return findCenterView(layoutManager, getVerticalHelper(layoutManager))
	} else if (layoutManager.canScrollHorizontally()) {
	    return findCenterView(layoutManager, getHorizontalHelper(layoutManager))
	}
	return null
}
private fun findCenterView(layoutManager: RecyclerView.LayoutManager, helper: OrientationHelper): View? {
	Log.i(TAG, "findCenterView")
	val childCount = layoutManager.childCount
	if (childCount == 0) return null
	// 最接近RecyclerView中心的ItemView
	var closestItemView: View? = null
	// RecyclerView的中心点
	val center = helper.startAfterPadding + helper.totalSpace / 2
	var absClosest = Int.MAX_VALUE
	
	for (i in 0 until childCount) {
	    val child = layoutManager.getChildAt(i)
	    // ItemView的中心点, 这里用Left是因为有ItemDecoration的存在,
	    val childCenter = child?.left!! + helper.getDecoratedMeasurement(child) / 2
	    val childDistance = Math.abs(childCenter - center)
	
	    // 找到最靠近RecyclerView中心的ItemView
	    if (childDistance < absClosest) {
	        absClosest = childDistance
	        closestItemView = child
	    }
	}
	return closestItemView
}
  • estimateNextPositionDiffForFling()方法:计算当前位置到目标对齐位置还差了几个ItemView的个数
    • calculateScrollDistance():计算RecyclerView的滚动距离
    • computeDistancePerChild():计算每个ItemView的滚动距离
private fun estimateNextPositionDiffForFling(layoutManager: RecyclerView.LayoutManager, helper: OrientationHelper, velocityX: Int, velocityY: Int): Int {
	Log.i(TAG, "estimateNextPositionDiffForFling")
	val distances = calculateScrollDistance(velocityX, velocityY)
	val distancePerChild = computeDistancePerChild(layoutManager, helper)
	if (distancePerChild <= 0) return 0
	
	val distance = if (Math.abs(distances[0]) > Math.abs(distances[1])) distances[0] else distances[1]
	return Math.round(distance / distancePerChild)
}
override fun calculateScrollDistance(velocityX: Int, velocityY: Int): IntArray {
    Log.i(TAG, "calculateScrollDistance")
    return super.calculateScrollDistance(velocityX, velocityY)
}
private fun computeDistancePerChild(layoutManager: RecyclerView.LayoutManager, helper: OrientationHelper): Float {
	Log.i(TAG, "computeDistancePerChild")
	var minPositionView : View ?= null
	var maxPositionView : View ?= null
	var minPosition = Integer.MAX_VALUE
	var maxPosition = Integer.MIN_VALUE
	val itemViewCount = layoutManager.childCount
	if (itemViewCount == 0) return INVALID_DISTANCE
	
	// 遍历所有ItemView, 找到最小位置和最大位置的ItemView,记录Position
	for (i in 0 until itemViewCount) {
	    val child = layoutManager.getChildAt(i) ?: continue
	    val position = layoutManager.getPosition(child)
	    if (position == RecyclerView.NO_POSITION) continue
	
	    if (position < minPosition) {
	        minPosition = position
	        minPositionView = child
	    }
	
	    if (position > maxPosition) {
	        maxPosition = position
	        maxPositionView = child
	    }
	}
	
	if (minPositionView == null || maxPositionView == null) return INVALID_DISTANCE
	
	// 计算最小位置和最大位置的ItemView离RecyclerView左边的距离
	val start = Math.min(helper.getDecoratedStart(minPositionView), helper.getDecoratedStart(maxPositionView))
	
	// 计算最小位置和最大位置的ItemView离RecyclerViewj右边的距离
	val end = Math.max(helper.getDecoratedEnd(minPositionView), helper.getDecoratedEnd(maxPositionView))
	
	// 计算最小位置和最大位置的ItemView的宽度
	val distance = end - start
	if (distance <= 0) return INVALID_DISTANCE
	return 1f * distance / (maxPosition - minPosition + 1)
}
  • onTargetFound()方法:找对对齐ItemView位置后回调
    • calculateDistanceToFinalSnap():计算最终需要滚动到对齐ItemView位置的距离
    • calculateTimeForDeceleration():计算最终需要滚动到对齐ItemView位置所花时间
override fun onTargetFound(targetView: View, state: RecyclerView.State, action: Action) {
	if (mRecyclerView == null) return
	Log.i(TAG, "onTargetFound")
	
	// 计算当前位置到目标位置的距离
	val snapDistances : IntArray? = calculateDistanceToFinalSnap(mRecyclerView?.layoutManager!!, targetView)
	val dx = snapDistances?.get(0) ?: 0
	val dy = snapDistances?.get(1) ?: 0
	// 这里增加滑动时间,可以使得滑动速度变慢
	val time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy))) * 10
	if (time > 0) {
	   // 这里传入的是LinearOutSlowInInterpolator(), 也就是先加速再减速的插值器
	   // 相比LinearSnapHelper中的DecelerateInterpolator, 这个插值器更符合自然滑动的效果
	   action.update(dx, dy, time, mInterpolator)
	}
}
override fun calculateDistanceToFinalSnap(layoutManager: RecyclerView.LayoutManager, targetView: View): IntArray? {
	Log.i(TAG, "calculateDistanceToFinalSnap")
	// 计算当前位置到目标位置的距离
	val out = IntArray(2)
	if (layoutManager.canScrollHorizontally()) {
	    out[0] = distanceToCenter(targetView, getHorizontalHelper(layoutManager)!!)
	} else {
	    out[0] = 0
	}
	
	if (layoutManager.canScrollVertically()) {
	    out[1] = distanceToCenter(targetView, getVerticalHelper(layoutManager)!!)
	} else {
	    out[1] = 0
	}
	return out
}
  • distanceToCenter()方法:计算目标对齐ItemView距离RecyclerView中心点的距离
private fun distanceToCenter(targetView: View, helper: OrientationHelper): Int {
   Log.i(TAG, "distanceToCenter")
   // 计算目标ItemView的中心点(ItemView包含ItemDecoration的部分 + ItemView的宽度/高度的一半)的
   val childCenter = helper.getDecoratedStart(targetView) + helper.getDecoratedMeasurement(targetView) / 2
   // 计算RecyclerView的中心点(RecyclerView减去Padding的部分 + RecyclerView的宽度/高度的一半)
   val containerCenter = helper.startAfterPadding + helper.totalSpace / 2
   return childCenter - containerCenter
}
  • 完整加减速的对齐工具类SnapHelper的代码
open class MySmoothSnapHelper : SnapHelper() {

    private val INVALID_DISTANCE = 1f // 无法计算有效对齐距离,返回这个值
    private val MILLISECONDS_PER_INCH = 25f // 滑动速度,每英寸25毫秒

    // 通过LayoutManager创建方向工具类,其中包含了RecyclerView的布局参数,包括padding,margin等
    private var mVerticalHelper : OrientationHelper ?= null
    private var mHorizontalHelper : OrientationHelper ?= null

    private var mRecyclerView : RecyclerView ?= null

    // 加速->减速插值器
    private val mInterpolator = LinearOutSlowInInterpolator()

    // 将RecyclerView交给SnapHelper, 计算惯性滑动后需要对齐的位置
    override fun attachToRecyclerView(recyclerView: RecyclerView?) {
        Log.i(TAG, "attachToRecyclerView")
        mRecyclerView = recyclerView
        super.attachToRecyclerView(recyclerView)
    }

    // 创建惯性滑动的Scroller
    override fun createScroller(layoutManager: RecyclerView.LayoutManager?): RecyclerView.SmoothScroller? {
        Log.i(TAG, "createScroller")
        if (layoutManager !is RecyclerView.SmoothScroller.ScrollVectorProvider) {
            return null
        }
        return object : LinearSmoothScroller(mRecyclerView?.context) {
            override fun onTargetFound(targetView: View, state: RecyclerView.State, action: Action) {
                if (mRecyclerView == null) return
                Log.i(TAG, "onTargetFound")

                // 计算当前位置到目标位置的距离
                val snapDistances : IntArray? = calculateDistanceToFinalSnap(mRecyclerView?.layoutManager!!, targetView)
                val dx = snapDistances?.get(0) ?: 0
                val dy = snapDistances?.get(1) ?: 0
                // 这里增加滑动时间,可以使得滑动速度变慢
                val time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy))) * 10
                if (time > 0) {
                    // 这里传入的是LinearOutSlowInInterpolator(), 也就是先加速再减速的插值器
                    // 相比LinearSnapHelper中的DecelerateInterpolator, 这个插值器更符合自然滑动的效果
                    action.update(dx, dy, time, mInterpolator)
                }
            }

            override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float {
                Log.i(TAG, "calculateSpeedPerPixel")
                // 计算滑动一个像素的时间
                return MILLISECONDS_PER_INCH / displayMetrics?.densityDpi!!
            }
        }
    }

    override fun calculateDistanceToFinalSnap(layoutManager: RecyclerView.LayoutManager, targetView: View): IntArray? {
        Log.i(TAG, "calculateDistanceToFinalSnap")
        // 计算当前位置到目标位置的距离
        val out = IntArray(2)
        if (layoutManager.canScrollHorizontally()) {
            out[0] = distanceToCenter(targetView, getHorizontalHelper(layoutManager)!!)
        } else {
            out[0] = 0
        }

        if (layoutManager.canScrollVertically()) {
            out[1] = distanceToCenter(targetView, getVerticalHelper(layoutManager)!!)
        } else {
            out[1] = 0
        }
        return out
    }

    private fun distanceToCenter(targetView: View, helper: OrientationHelper): Int {
        Log.i(TAG, "distanceToCenter")
        // 计算目标ItemView的中心点(ItemView包含ItemDecoration的部分 + ItemView的宽度/高度的一半)的
        val childCenter = helper.getDecoratedStart(targetView) + helper.getDecoratedMeasurement(targetView) / 2
        // 计算RecyclerView的中心点(RecyclerView减去Padding的部分 + RecyclerView的宽度/高度的一半)
        val containerCenter = helper.startAfterPadding + helper.totalSpace / 2
        return childCenter - containerCenter
    }

    override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
        Log.i(TAG, "findSnapView")
        if (layoutManager!!.canScrollVertically()) {
            return findCenterView(layoutManager, getVerticalHelper(layoutManager))
        } else if (layoutManager.canScrollHorizontally()) {
            return findCenterView(layoutManager, getHorizontalHelper(layoutManager))
        }
        return null
    }

    private fun findCenterView(layoutManager: RecyclerView.LayoutManager, helper: OrientationHelper): View? {
        Log.i(TAG, "findCenterView")
        val childCount = layoutManager.childCount
        if (childCount == 0) return null
        // 最接近RecyclerView中心的ItemView
        var closestItemView: View? = null
        // RecyclerView的中心点
        val center = helper.startAfterPadding + helper.totalSpace / 2
        var absClosest = Int.MAX_VALUE

        for (i in 0 until childCount) {
            val child = layoutManager.getChildAt(i)
            // ItemView的中心点, 这里用Left是因为有ItemDecoration的存在,
            val childCenter = child?.left!! + helper.getDecoratedMeasurement(child) / 2
            val childDistance = Math.abs(childCenter - center)

            // 找到最靠近RecyclerView中心的ItemView
            if (childDistance < absClosest) {
                absClosest = childDistance
                closestItemView = child
            }
        }
        return closestItemView
    }
    override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager?, velocityX: Int, velocityY: Int): Int {
        Log.i(TAG, "findTargetSnapPosition")
        // 判断layoutManager是否实现了RecyclerView.SmoothScroller.ScrollVectorProvider这个接口
        if (layoutManager !is RecyclerView.SmoothScroller.ScrollVectorProvider) return RecyclerView.NO_POSITION

        // 判断ItemView个数是否小于等于0
        val itemCount = layoutManager.itemCount
        if (itemCount == 0) return RecyclerView.NO_POSITION

        // 找到需要对齐的ItemView
        val currentView = findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION

        // 获取需要对齐ItemView的位置
        val currentPosition = layoutManager.getPosition(currentView)
        if (currentPosition == RecyclerView.NO_POSITION) return RecyclerView.NO_POSITION

        // 判断layoutManager的布局方向
        val vectorProvider = layoutManager as RecyclerView.SmoothScroller.ScrollVectorProvider
        val vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1) ?: return RecyclerView.NO_POSITION

        // 计算水平, 垂直方向最多能惯性滑动的ItemView个数,在当前ItemView的位置上,进行加减Position操作
        var maxHorizontalItemViewCount: Int
        var maxVerticalItemViewCount: Int

        if (layoutManager.canScrollHorizontally()) {
            maxHorizontalItemViewCount = estimateNextPositionDiffForFling(layoutManager, getHorizontalHelper(layoutManager), velocityX, 0)
            var sign = Math.signum(velocityX.toFloat())
            if (sign == 0f) sign = 1f
            // 限制最多能惯性滑动的ItemView个数,限制滑动的 ItemView 的个数在 1 到 2 之间
            maxHorizontalItemViewCount = (sign * Math.min(Math.max(Math.abs(maxHorizontalItemViewCount), 0), 2)).toInt()
            if (vectorForEnd.x < 0) {
                maxHorizontalItemViewCount = - maxHorizontalItemViewCount
            }
        }else{
            maxHorizontalItemViewCount = 0
        }

        if (layoutManager.canScrollVertically()) {
            maxVerticalItemViewCount = estimateNextPositionDiffForFling(layoutManager, getVerticalHelper(layoutManager), 0, velocityY)
            var sign = Math.signum(velocityY.toFloat())
            if (sign == 0f) sign = 1f
            // 限制最多能惯性滑动的ItemView个数,限制滑动的 ItemView 的个数在 1 到 2 之间
            maxVerticalItemViewCount = (sign * Math.min(Math.max(Math.abs(maxVerticalItemViewCount), 0), 2)).toInt()
            if (vectorForEnd.y < 0) {
                maxVerticalItemViewCount = - maxVerticalItemViewCount
            }
        }else{
            maxVerticalItemViewCount = 0
        }

        // 根据滑动的方向,计算出最终的 ItemView 个数
        val finalItemCount = if(layoutManager.canScrollHorizontally()){
            maxHorizontalItemViewCount
        }else{
            maxVerticalItemViewCount
        }

        if (finalItemCount == 0) return RecyclerView.NO_POSITION

        // 确定最终的对齐位置,并做边界处理
        var targetPosition = currentPosition + finalItemCount
        if (targetPosition < 0) targetPosition = 0
        if (targetPosition >= layoutManager.itemCount) targetPosition = layoutManager.itemCount - 1
        return targetPosition
    }

    override fun calculateScrollDistance(velocityX: Int, velocityY: Int): IntArray {
        Log.i(TAG, "calculateScrollDistance")
        return super.calculateScrollDistance(velocityX, velocityY)
    }

    private fun estimateNextPositionDiffForFling(layoutManager: RecyclerView.LayoutManager, helper: OrientationHelper, velocityX: Int, velocityY: Int): Int {
        Log.i(TAG, "estimateNextPositionDiffForFling")
        val distances = calculateScrollDistance(velocityX, velocityY)
        val distancePerChild = computeDistancePerChild(layoutManager, helper)
        if (distancePerChild <= 0) return 0

        val distance = if (Math.abs(distances[0]) > Math.abs(distances[1])) distances[0] else distances[1]
        return Math.round(distance / distancePerChild)
    }

    private fun computeDistancePerChild(layoutManager: RecyclerView.LayoutManager, helper: OrientationHelper): Float {
        Log.i(TAG, "computeDistancePerChild")
        var minPositionView : View ?= null
        var maxPositionView : View ?= null
        var minPosition = Integer.MAX_VALUE
        var maxPosition = Integer.MIN_VALUE
        val itemViewCount = layoutManager.childCount
        if (itemViewCount == 0) return INVALID_DISTANCE

        // 遍历所有ItemView, 找到最小位置和最大位置的ItemView,记录Position
        for (i in 0 until itemViewCount) {
            val child = layoutManager.getChildAt(i) ?: continue
            val position = layoutManager.getPosition(child)
            if (position == RecyclerView.NO_POSITION) continue

            if (position < minPosition) {
                minPosition = position
                minPositionView = child
            }

            if (position > maxPosition) {
                maxPosition = position
                maxPositionView = child
            }
        }

        if (minPositionView == null || maxPositionView == null) return INVALID_DISTANCE

        // 计算最小位置和最大位置的ItemView离RecyclerView左边的距离
        val start = Math.min(helper.getDecoratedStart(minPositionView), helper.getDecoratedStart(maxPositionView))

        // 计算最小位置和最大位置的ItemView离RecyclerViewj右边的距离
        val end = Math.max(helper.getDecoratedEnd(minPositionView), helper.getDecoratedEnd(maxPositionView))

        // 计算最小位置和最大位置的ItemView的宽度
        val distance = end - start
        if (distance <= 0) return INVALID_DISTANCE
        return 1f * distance / (maxPosition - minPosition + 1)
    }

    private fun getVerticalHelper(layoutManager: RecyclerView.LayoutManager): OrientationHelper {
        Log.i(TAG, "getVerticalHelper")
        if (mVerticalHelper == null || mVerticalHelper?.layoutManager != layoutManager) {
            mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager)
        }
        return mVerticalHelper!!
    }

    private fun getHorizontalHelper(layoutManager: RecyclerView.LayoutManager): OrientationHelper {
        Log.i(TAG, "getHorizontalHelper")
        if (mHorizontalHelper == null || mHorizontalHelper!!.layoutManager != layoutManager) {
            mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager)
        }
        return mHorizontalHelper!!
    }
}

Activity代码

  • 第一次findSnapView:正常滑动停止后触发,需要找到对齐的View
  • 第二次findSnapView:惯性滑动停止后触发,需要找到对齐的View
const val TAG = "Yang"
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val numberList = List(10){it}
        val layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
        val mRv = findViewById<RecyclerView>(R.id.recyclerView)
        val mAdapter = MyAdapter(numberList)

        // 添加 ItemDecoration
        mRv.addItemDecoration(SpaceItemDecoration(dpToPx(this, 25f), dpToPx(this, 100f)))
        // 添加 LinearSnapHelper
        val linearSnapHelper = object : MySmoothSnapHelper() {
            override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
                val snapView = super.findSnapView(layoutManager)
                val snapPosition = snapView?.let {mRv.getChildAdapterPosition(it) }
                snapPosition?.let {
                    if (snapPosition != RecyclerView.NO_POSITION) {
                        mAdapter.setSelectedPosition(snapPosition)
                    }
                }
                return snapView
            }
        }
        linearSnapHelper.attachToRecyclerView(mRv)
        mRv?.layoutManager = layoutManager
        mRv?.adapter = mAdapter
    }

    fun dpToPx(context: Context, dp: Float): Int {
        val metrics = context.resources.displayMetrics
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics).toInt()
    }
}

// log
2024-06-21 01:18:42.794 17860-17860 Yang                    I  attachToRecyclerView
2024-06-21 01:18:45.412 17860-17860 Yang                    I  createScroller
2024-06-21 01:18:45.413 17860-17860 Yang                    I  findTargetSnapPosition
2024-06-21 01:18:45.413 17860-17860 Yang                    I  findSnapView
2024-06-21 01:18:45.413 17860-17860 Yang                    I  getHorizontalHelper
2024-06-21 01:18:45.413 17860-17860 Yang                    I  findCenterView
2024-06-21 01:18:45.413 17860-17860 Yang                    I  getHorizontalHelper
2024-06-21 01:18:45.413 17860-17860 Yang                    I  estimateNextPositionDiffForFling
2024-06-21 01:18:45.413 17860-17860 Yang                    I  calculateScrollDistance
2024-06-21 01:18:45.413 17860-17860 Yang                    I  computeDistancePerChild
2024-06-21 01:18:45.430 17860-17860 Yang                    I  onTargetFound
2024-06-21 01:18:45.430 17860-17860 Yang                    I  calculateDistanceToFinalSnap
2024-06-21 01:18:45.430 17860-17860 Yang                    I  getHorizontalHelper
2024-06-21 01:18:45.430 17860-17860 Yang                    I  distanceToCenter
2024-06-21 01:18:45.430 17860-17860 Yang                    I  calculateSpeedPerPixel
2024-06-21 01:18:46.400 17860-17860 Yang                    I  findSnapView
2024-06-21 01:18:46.400 17860-17860 Yang                    I  getHorizontalHelper
2024-06-21 01:18:46.400 17860-17860 Yang                    I  findCenterView
2024-06-21 01:18:46.400 17860-17860 Yang                    I  calculateDistanceToFinalSnap
2024-06-21 01:18:46.400 17860-17860 Yang                    I  getHorizontalHelper
2024-06-21 01:18:46.400 17860-17860 Yang                    I  distanceToCenter

效果图

在这里插入图片描述