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

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"><TextViewandroid: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.RecyclerViewandroid: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_POSITIONoverride 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 = selectedPositionselectedPosition = positionnotifyItemChanged(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.REDstyle = Paint.Style.FILL}var spacePadding = 0override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {super.getItemOffsets(outRect, view, parent, state)// 正常Item的DecorationoutRect.left = spaceSizeoutRect.right = spaceSize// 第一个和最后一个Item的DecorationspacePadding = (parent.measuredWidth / 2 - itemSize / 2)val size = parent.adapter?.itemCount ?: 0val 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.childCountfor (i in 0 until childCount) {val child = parent.getChildAt(i)val position = parent.getChildAdapterPosition(child)val params = child.layoutParams as RecyclerView.LayoutParamsvar left : Intvar right : Intvar top : Intvar bottom : Intif (position == 0) {left = child.left - params.leftMargin - spacePaddingright = child.right + params.rightMargin + spaceSizetop = child.top - params.topMarginbottom = child.bottom + params.bottomMargin} else if (position == parent.adapter?.itemCount!! - 1) {left = child.left - params.leftMargin - spaceSizeright = child.right + params.rightMargin + spacePaddingtop = child.top - params.topMarginbottom = child.bottom + params.bottomMargin} else {// 绘制其他 Item 的装饰left = child.left - params.leftMargin - spaceSizeright = child.right + params.rightMargin + spaceSizetop = child.top - params.topMarginbottom = 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 = recyclerViewsuper.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) returnLog.i(TAG, "onTargetFound")// 计算当前位置到目标位置的距离val snapDistances : IntArray? = calculateDistanceToFinalSnap(mRecyclerView?.layoutManager!!, targetView)val dx = snapDistances?.get(0) ?: 0val dy = snapDistances?.get(1) ?: 0// 这里增加滑动时间,可以使得滑动速度变慢val time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy))) * 10if (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个数是否小于等于0val itemCount = layoutManager.itemCountif (itemCount == 0) return RecyclerView.NO_POSITION// 找到需要对齐的ItemViewval 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.ScrollVectorProviderval vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1) ?: return RecyclerView.NO_POSITIONLog.i(TAG, "findTargetSnapPosition")// 计算水平, 垂直方向最多能惯性滑动的ItemView个数,在当前ItemView的位置上,进行加减Position操作var maxHorizontalItemViewCount: Intvar maxVerticalItemViewCount: Intif (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 + finalItemCountif (targetPosition < 0) targetPosition = 0if (targetPosition >= layoutManager.itemCount) targetPosition = layoutManager.itemCount - 1return 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.childCountif (childCount == 0) return null// 最接近RecyclerView中心的ItemViewvar closestItemView: View? = null// RecyclerView的中心点val center = helper.startAfterPadding + helper.totalSpace / 2var absClosest = Int.MAX_VALUEfor (i in 0 until childCount) {val child = layoutManager.getChildAt(i)// ItemView的中心点, 这里用Left是因为有ItemDecoration的存在,val childCenter = child?.left!! + helper.getDecoratedMeasurement(child) / 2val childDistance = Math.abs(childCenter - center)// 找到最靠近RecyclerView中心的ItemViewif (childDistance < absClosest) {absClosest = childDistanceclosestItemView = 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 0val 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 ?= nullvar maxPositionView : View ?= nullvar minPosition = Integer.MAX_VALUEvar maxPosition = Integer.MIN_VALUEval itemViewCount = layoutManager.childCountif (itemViewCount == 0) return INVALID_DISTANCE// 遍历所有ItemView, 找到最小位置和最大位置的ItemView,记录Positionfor (i in 0 until itemViewCount) {val child = layoutManager.getChildAt(i) ?: continueval position = layoutManager.getPosition(child)if (position == RecyclerView.NO_POSITION) continueif (position < minPosition) {minPosition = positionminPositionView = child}if (position > maxPosition) {maxPosition = positionmaxPositionView = 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 - startif (distance <= 0) return INVALID_DISTANCEreturn 1f * distance / (maxPosition - minPosition + 1)
}
  • onTargetFound()方法:找对对齐ItemView位置后回调
    • calculateDistanceToFinalSnap():计算最终需要滚动到对齐ItemView位置的距离
    • calculateTimeForDeceleration():计算最终需要滚动到对齐ItemView位置所花时间
override fun onTargetFound(targetView: View, state: RecyclerView.State, action: Action) {if (mRecyclerView == null) returnLog.i(TAG, "onTargetFound")// 计算当前位置到目标位置的距离val snapDistances : IntArray? = calculateDistanceToFinalSnap(mRecyclerView?.layoutManager!!, targetView)val dx = snapDistances?.get(0) ?: 0val dy = snapDistances?.get(1) ?: 0// 这里增加滑动时间,可以使得滑动速度变慢val time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy))) * 10if (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 / 2return 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 ?= nullprivate var mHorizontalHelper : OrientationHelper ?= nullprivate var mRecyclerView : RecyclerView ?= null// 加速->减速插值器private val mInterpolator = LinearOutSlowInInterpolator()// 将RecyclerView交给SnapHelper, 计算惯性滑动后需要对齐的位置override fun attachToRecyclerView(recyclerView: RecyclerView?) {Log.i(TAG, "attachToRecyclerView")mRecyclerView = recyclerViewsuper.attachToRecyclerView(recyclerView)}// 创建惯性滑动的Scrolleroverride 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) returnLog.i(TAG, "onTargetFound")// 计算当前位置到目标位置的距离val snapDistances : IntArray? = calculateDistanceToFinalSnap(mRecyclerView?.layoutManager!!, targetView)val dx = snapDistances?.get(0) ?: 0val dy = snapDistances?.get(1) ?: 0// 这里增加滑动时间,可以使得滑动速度变慢val time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy))) * 10if (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 / 2return 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.childCountif (childCount == 0) return null// 最接近RecyclerView中心的ItemViewvar closestItemView: View? = null// RecyclerView的中心点val center = helper.startAfterPadding + helper.totalSpace / 2var absClosest = Int.MAX_VALUEfor (i in 0 until childCount) {val child = layoutManager.getChildAt(i)// ItemView的中心点, 这里用Left是因为有ItemDecoration的存在,val childCenter = child?.left!! + helper.getDecoratedMeasurement(child) / 2val childDistance = Math.abs(childCenter - center)// 找到最靠近RecyclerView中心的ItemViewif (childDistance < absClosest) {absClosest = childDistanceclosestItemView = 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个数是否小于等于0val itemCount = layoutManager.itemCountif (itemCount == 0) return RecyclerView.NO_POSITION// 找到需要对齐的ItemViewval 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.ScrollVectorProviderval vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1) ?: return RecyclerView.NO_POSITION// 计算水平, 垂直方向最多能惯性滑动的ItemView个数,在当前ItemView的位置上,进行加减Position操作var maxHorizontalItemViewCount: Intvar maxVerticalItemViewCount: Intif (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 + finalItemCountif (targetPosition < 0) targetPosition = 0if (targetPosition >= layoutManager.itemCount) targetPosition = layoutManager.itemCount - 1return 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 0val 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 ?= nullvar maxPositionView : View ?= nullvar minPosition = Integer.MAX_VALUEvar maxPosition = Integer.MIN_VALUEval itemViewCount = layoutManager.childCountif (itemViewCount == 0) return INVALID_DISTANCE// 遍历所有ItemView, 找到最小位置和最大位置的ItemView,记录Positionfor (i in 0 until itemViewCount) {val child = layoutManager.getChildAt(i) ?: continueval position = layoutManager.getPosition(child)if (position == RecyclerView.NO_POSITION) continueif (position < minPosition) {minPosition = positionminPositionView = child}if (position > maxPosition) {maxPosition = positionmaxPositionView = 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 - startif (distance <= 0) return INVALID_DISTANCEreturn 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)// 添加 ItemDecorationmRv.addItemDecoration(SpaceItemDecoration(dpToPx(this, 25f), dpToPx(this, 100f)))// 添加 LinearSnapHelperval 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 = layoutManagermRv?.adapter = mAdapter}fun dpToPx(context: Context, dp: Float): Int {val metrics = context.resources.displayMetricsreturn 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.rhkb.cn/news/359262.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大腾智能,基于云原生的国产工业协同平台

大腾智能是一家基于云原生的国产工业软件与数字化协同平台&#xff0c;专注于推动企业数字化转型与升级&#xff0c;为企业提供一系列专业、高效的云原生数字化软件及方案&#xff0c;推动产品设计、生产及营销展示的革新&#xff0c;实现可持续发展。 大腾智能旗下产品 3D模型…

美的集团员工自爆工资+年终奖收入明细,网友说:这待遇,老婆根本不让跳槽!...

发现需求&#xff1a;研究与实践是关键 在任何领域&#xff0c;只要深入研究&#xff0c;就会发现无数的需求。如果没有发现需求&#xff0c;那只能说明对行业的了解还不够透彻。学校通过考试发现学生的问题&#xff0c;职场上也一样&#xff0c;通过不断实践发现问题。理论知识…

XSS+CSRF组合拳

目录 简介 如何进行实战 进入后台创建一个新用户进行接口分析 构造注入代码 寻找XSS漏洞并注入 小结 简介 &#xff08;案例中将使用cms靶场来进行演示&#xff09; 在实战中CSRF利用条件十分苛刻&#xff0c;因为我们需要让受害者点击我们的恶意请求不是一件容易的事情…

196.每日一题:检测大写字母(力扣)

代码解决 class Solution { public:bool detectCapitalUse(string word) {int capitalCount 0;int n word.size();// 统计大写字母的数量for (char c : word) {if (isupper(c)) {capitalCount;}}// 检查是否满足三种情况之一if (capitalCount n) {// 全部字母都是大写return…

Adobe Premiere 视频编辑软件下载安装,pr 全系列资源分享!

Adobe Premiere以其强大的功能、灵活的操作和卓越的性能&#xff0c;成为视频编辑领域的佼佼者。 在剪辑方面&#xff0c;Adobe Premiere提供了强大而灵活的工具集。用户可以在直观的时间线上对视频进行精细的裁剪、剪辑和合并操作。无论是快速剪辑短片&#xff0c;还是精心打造…

我真是反感那些叉劈。

再转一下&#xff0c;想看的自己提取吧。

MyBatis 动态 SQL怎么使用?

引言&#xff1a;在现代的软件开发中&#xff0c;数据库操作是任何应用程序的核心部分之一。而在 Java 开发领域&#xff0c;MyBatis 作为一款优秀的持久层框架&#xff0c;以其简洁的配置和强大的灵活性被广泛应用。动态 SQL 允许开发人员根据不同的条件和场景动态地生成和执行…

前端也需要知道的一些常用linux命令

前端也需要知道的一些常用linux命令 1.问题背景2.连接工具&#xff08;SecureCRT_Portable&#xff09;a.下载工具b.连接服务器c.登录到root账户 3.基本命令a.cd命令和cd ..b.ll命令和ls命令c:cp命令d.rm命令e:rz命令f.unzip命令g.mv命令h.pwd命令&#xff08;这里没有用到&…

【CPP】交换排序:冒泡排序、快速排序

目录 1.冒泡排序简介代码分析 2.快速排序2.1霍尔版本简介代码分析 2.2挖坑版本2.3前后指针版本2.4非递归的快排思路代码 什么是交换排序&#xff1f; 基本思想&#xff1a;所谓 交换&#xff0c;就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置&#xff0…

零基础STM32单片机编程入门(一)初识STM32单片机

文章目录 一.概要二.单片机型号命名规则三.STM32F103系统架构四.STM32F103C8T6单片机启动流程五.STM32F103C8T6单片机主要外设资源六.编程过程中芯片数据手册的作用1.单片机外设资源情况2.STM32单片机内部框图3.STM32单片机管脚图4.STM32单片机每个管脚可配功能5.单片机功耗数据…

竞赛选题 python opencv 深度学习 指纹识别算法实现

1 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; python opencv 深度学习 指纹识别算法实现 &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&#xff1a;3分工作量&#xff1a;4分创新点&#xff1a;4分 该项目较为新颖…

【STM32】STM32通过I2C实现温湿度采集与显示

目录 一、I2C总线通信协议 1.I2C通信特征 2.I2C总线协议 3.软件I2C和硬件I2C 二、stm32通过I2C实现温湿度&#xff08;AHT20&#xff09;采集 1.stm32cube配置 RCC配置&#xff1a; SYS配置&#xff1a; I2C1配置&#xff1a; USART1配置&#xff1a; GPIO配置&#…

day50 1143.最长公共子序列 1035.不相交的线 53. 最大子序和 392.判断子序列

1143. 最长公共子序列 提示 给定两个字符串 text1 和 text2&#xff0c;返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 &#xff0c;返回 0 。 一个字符串的 子序列 是指这样一个新的字符串&#xff1a;它是由原字符串在不改变字符的相对顺序的情况下删…

Excel 解析十六进制并查找

A1 格由多个人名及其考勤情况组成&#xff0c;比如&#xff0c;c 是十六进制的 1100&#xff0c;表示第 1、2 天到场&#xff0c;第 3、4 天缺席。目前只有 4 天的考勤。 AB1alice,c,bob,7,clara,a,mike,9/input: name and presence22/input: the day to be queried 要求根据…

【Linux】基础 I / O

目录 一、C文件操作函数&#xff1a; 二、输入 / 输出 / 错误流&#xff1a; 三、系统文件 I/O open函数&#xff1a; write&#xff1a; read&#xff1a; close&#xff1a; 具体应用&#xff1a; 四、文件描述符(fd): 1、概念&#xff1a; 2、文件管理&#xff1…

计算机网络 —— 网络字节序

网络字节序 1、网络字节序 (Network Byte Order)和本机转换 1、大端、小端字节序 “大端” 和” 小端” 表示多字节值的哪一端存储在该值的起始地址处&#xff1b;小端存储在起始地址处&#xff0c;即是小端字节序&#xff1b;大端存储在起始地址处&#xff0c;即是大端字节…

Pytorch深度解析:Transformer嵌入层源码逐行解读

前言 本部分博客需要先阅读博客&#xff1a; 《Transformer实现以及Pytorch源码解读&#xff08;一&#xff09;-数据输入篇》 作为知识储备。 Embedding使用方式 如下面的代码中所示&#xff0c;embedding一般是先实例化nn.Embedding(vocab_size, embedding_dim)。实例化的…

【shell脚本速成】mysql备份脚本

文章目录 案例需求脚本应用场景&#xff1a;解决问题脚本思路实现代码 &#x1f308;你好呀&#xff01;我是 山顶风景独好 &#x1f388;欢迎踏入我的博客世界&#xff0c;能与您在此邂逅&#xff0c;真是缘分使然&#xff01;&#x1f60a; &#x1f338;愿您在此停留的每一刻…

更改ip后还被封是ip质量的原因吗?

不同的代理IP的质量相同&#xff0c;一般来说可以根据以下几个因素来进行判断&#xff1a; 1.可用率 可用率就是提取的这些代理IP中可以正常使用的比率。假如我们无法使用某个代理IP请求目标网站或者请求超时&#xff0c;那么就代表这个代理不可用&#xff0c;一般来说免费代…

最强铁基超导磁体诞生!科学家基于机器学习设计新研究体系,磁场强度超过先前记录2.7倍

超导现象&#xff0c;自 1911 年被发现以来&#xff0c;始终保持着前沿性与高价值&#xff0c;吸引了大批学者投身其研究中。超导现象是指某些材料在低于特定温度时电阻突然降为零&#xff0c;这不仅是材料学的革命性突破&#xff0c;也为电力传输、磁悬浮交通和医疗成像等领域…