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

news2024/11/26 19:25:36

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) {
                Log.i(TAG, "position = $position")
                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

效果图

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1845462.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ubuntu 安装 CloudCompare

步骤&#xff1a; sudo apt install flatpakflatpak install flathub org.cloudcompare.CloudCompare此时会有报错&#xff1a; error: No remote refs found similar to ‘flathub’执行 flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.fla…

【大模型驯化-gradio】成功解决gradio出现httpcore.ReadTimeout: timed out问题

【大模型驯化-gradio】成功解决gradio出现httpcore.ReadTimeout: timed out问题 本次修炼方法请往下查看 &#x1f308; 欢迎莅临我的个人主页 &#x1f448;这里是我工作、学习、实践 IT领域、真诚分享 踩坑集合&#xff0c;智慧小天地&#xff01; &#x1f387; 免费获取…

LeetCode刷题之HOT100之LRU缓存

2024/6/21 酷暑难耐&#xff0c;离开空调我将不知道能否《活着》&#xff0c;昨天跑步感觉全身的热无法排出去&#xff0c;出门那种热浪一阵一阵打过来&#xff0c;一点风都舍不得给我。早早的来到实验室&#xff0c;也没多早&#xff0c;九点哈哈&#xff0c;做题啦&#xff0…

Linux【实操篇-文件目录类命令】

05【实操篇-文件目录类命令】 1.pwd 显示当前工作目录的绝对路径 pwd:print working directory 打印工作目录 到现在为止&#xff0c;我们还不知道自己在系统的什么地方。在浏览器上&#xff0c;我们能够通过导航栏上的url&#xff0c;了解到自己在互联网上的具体坐标。相似的…

GNSS是什么意思?探索全球导航卫星系统的应用与发展

GNSS是什么意思&#xff1f;GNSS是全球导航卫星系统&#xff08;Global Navigation Satellite System&#xff09;的缩写&#xff0c;它借助于多颗位于地球轨道上的卫星&#xff0c;通过接收和处理卫星信号来实现高精度的全球定位和导航。GNSS系统不仅包括美国的GPS&#xff08…

C# 语言在AGI 赛道上能做什么

自从2022年11月OpenAI正式对外发布ChatGPT依赖&#xff0c;AGI 这条赛道上就挤满了重量级的选手&#xff0c;各大头部公司纷纷下场布局。原本就在机器学习、深度学习领域占据No.1的Python语言更是继续稳固了自己AI一哥的位置。凭借着Microsoft 和 OpenAI 长期以来一直是紧密相连…

CVPR 2024 以物体为中心的多感知具身大语言模型

CVPR 2024发表了关于多感知以对象为中心的具身大型语言模型&#xff08;MultiPLY&#xff09;的研究论文&#xff0c;该模型在3D环境中进行编码和交互。 提出MultiPLY是一个多感知的具身大型语言模型&#xff08;LLM&#xff09;&#xff0c;能够将视觉、听觉、触觉和温度等多…

【netty】三万字详解!JAVA高性能通信框架,关于netty,看这一篇就够了

目录 1.概述 2.hello world 3.EventLoop 4.channel 4.1.同步 4.2.异步 4.3.调试 4.4.关闭 4.5.为什么要用异步 5.future 6.promise 7.pipeline 8.byteBuf 8.1.创建 8.2.内存模式和池化 8.2.1.内存模式 8.2.2.池化 8.3.组成 8.4.操作 8.4.1.读写 8.4.2.释放…

钢琴块小游戏(附源码)

代码结构 app.png是游戏运行主界面的图片&#xff08;可以加载自己喜欢的主界面图片&#xff09; Assets文件夹里面装的是一些需要用到的游戏图片 全部都可以替换为自己喜欢的图片 Fonts里面装的是 Sounds文件夹里面装的是 一 . 主程序代码 1.运行这个代码使得游戏开始 2.主界面…

深入理解Vue3.js响应式系统设计之调度执行

如果您觉得这篇文章有帮助的话&#xff01;给个点赞和评论支持下吧&#xff0c;感谢~ 作者&#xff1a;前端小王hs 阿里云社区博客专家/清华大学出版社签约作者/csdn百万访问前端博主/B站千粉前端up主 此篇文章是博主于2022年学习《Vue.js设计与实现》时的笔记整理而来 书籍&a…

Linux 一键部署 Nginx1.26.1 + ModSecurity3

前言 ModSecurity 是 Apache 基金会的一个开源、高性能的 Web 应用程序防火墙(WAF),它提供了强大的安全规则引擎,用于检测和阻止各种攻击行为,如 SQL 注入、XSS 跨站点脚本攻击等。而 nginx 是一个高性能的 Web 服务器,常用于处理大量的并发请求,具有很高的负载均衡能力…

知网期刊《新课程导学》投稿要求及收稿方向

知网期刊《新课程导学》投稿要求及收稿方向 知网期刊《新课程导学》作为一份专注于教育领域的学术期刊&#xff0c;一直以来都致力于为广大学术研究者提供一个高质量、高水平的学术交流平台。为了保证期刊的学术质量&#xff0c;编辑部对投稿要求和收稿方向有着严格的规定。 首…

Postman接口测试详解与进阶

Postman是一个功能强大的接口测试工具&#xff0c;它主要用于模拟用户发起的各类HTTP请求&#xff0c;从而验证响应中的结果数据是否和预期值相匹配。以下是关于Postman的详细介绍&#xff1a; Postman是一个功能全面、使用便捷、支持多种HTTP请求类型、提供丰富的测试数据和配…

铝型材挤压车间的数字孪生应用

图扑利用数字孪生技术&#xff0c;在铝型材挤压车间实现了生产线的全方位实时监控和优化。通过高精度三维建模和数据可视化&#xff0c;提升了效率和管理透明度&#xff0c;促进了智能制造和资源配置的优化。

【2024亲测无坑】在Centos.7虚拟机上安装Oracle 19C

目录 一、安装环境准备 1、linux虚拟机安装 2、虚拟机快照 3、空间检查&软件上传 二、Oracle软件安装 1.preinstall安装及其他配置准备 2.oracle安装 三、数据库实例的安装 1.netca——网络配置助手 2.dbca——数据库配置助手 四、ORACLE 19C 在linux centos 7上…

c++qt合并两张灰度图像

需求&#xff1a;将两张尺寸相同的灰度图像进行合并&#xff0c;合并后的图像&#xff0c;每个像素点灰度值为两张原图对应像素点灰度值之和。若超过255&#xff0c;则最大为255。 方法一&#xff1a; 将图像读取为cv::Mat&#xff0c;再调用opencv的cv::add方法&#xff0c;进…

Electron快速入门(一):用VS Code快速创建html+js+css编写的项目

创建一个文件夹&#xff08;例如&#xff1a;start或者create-electron 都是小写英文字母有的插件才不会报错&#xff09;&#xff0c;并进入该文件夹&#xff0c;打开 vscode创建3个文件&#xff1a; 1. 名为 main.js 的文件是主进程 // main.js//用于控制应用程序寿命和创建…

网络安全的双刃守护:揭秘双算法SSL证书的智慧盾牌

双算法SSL证书&#xff0c;这一科技与智慧的结晶&#xff0c;如同夜空中最亮的双子星&#xff0c;照亮了数据传输的幽径。它不仅继承了传统SSL证书的精髓&#xff0c;确保信息在传递过程中的私密与完整&#xff0c;更创新性地融合了两种顶尖加密算法——RSA与SM2&#xff0c;犹…

gateway整合sentinel限流

官方文档&#xff1a;https://github.com/alibaba/Sentinel/wiki/%E7%BD%91%E5%85%B3%E9%99%90%E6%B5%81 从 1.6.0 版本开始&#xff0c;Sentinel 提供了 Spring Cloud Gateway 的适配模块&#xff0c;可以提供两种资源维度的限流&#xff1a; route 维度&#xff1a;即在 Spr…

STM32的通用定时器中断编程

如果遇到需要单片机产生严格时序的场景&#xff08;比如DAC输出特定模拟信号&#xff0c;GPIO口控制模拟开关&#xff09;&#xff0c;延时函数可能就无法胜任了。最近在工作时公司上级教会了我使用“门票”思维&#xff08;中断标志位)编写单片机裸机程序&#xff0c;今天写一…