在 recyclerView 列表中,滑动到边界后,继续滑动,会发现自带一个阻尼效果,但是往往不能满足产品需求,需要自定义
比如拉伸的最大距离,或者拉伸的位置
模仿安卓最近任务列表,列表中item可以上下左右滑动,并且左右下方向滑动到边界后会产生阻尼效果,随着拉伸的距离增大而增大
方案1:这里可以自定义阻尼算法,比如粗糙的方式计算
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.test.recent_task.utils import android.util.Log import kotlin.math.max /** * @author liuzhen * 拖拽阻尼 */ class DragDampingHelper { companion object { private const val TAG = "DragDampingHelper" } // 最大拉动值 private var maxNum = 100f // 前一次记录位置 private var previousNum = 0f // 起始位置 private val startNum = 0f // 两次移动间移动的相对距离 private var deltaNum = 0f // 结果 private var result = 0f private fun getDampingNum(y: Float, isUp: Boolean): Float { // 抬手或者复位时需要清除缓存数据重新计算 if (isUp || y == 0f) { result = 0f previousNum = 0f return 0f } deltaNum = y - previousNum previousNum = y //计算阻尼 var distance = y - startNum if (distance < 0) { distance *= -1f } var damping = (maxNum - distance) / maxNum // 1 ~ 0.1 damping = max(damping, 0.1f) if (y - startNum < 0) { damping = 1 - damping } result += deltaNum * damping log("getDampingNum previousNum $previousNum deltaNum $deltaNum result $result damping $damping") return result } /** 阻尼拉动 */ fun pull(y: Float, isUp: Boolean, maxNum: Float): Float { this.maxNum = maxNum * 2 val result = if (y >= 0) getDampingNum(y, isUp) else { getDampingNum(0f, false) y } log("pull y $y isUp $isUp result $result") return if (result >= maxNum) maxNum else result } private fun log(str: String) = Log.w(TAG, str) }
缺点很明显,如果只是单方向滑动,没什么问题,但是如果按下后来回滑动反人类操作,适配难度较大,这里是直接归零处理,只有往一个方向时才开始计算阻尼
方案2:使用 recyclerView 自带的 EdgeEffect
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.test.recent_task.utils import android.animation.ValueAnimator import android.util.Log import android.view.animation.DecelerateInterpolator import android.widget.EdgeEffect import androidx.recyclerview.widget.RecyclerView import kotlin.math.abs import kotlin.math.max import kotlin.math.min /** * @author liuzhen * 滑动阻尼 */ class DampingEdgeEffect : RecyclerView.EdgeEffectFactory() { private var effect: CustomEdgeEffect? = null override fun createEdgeEffect(view: RecyclerView, direction: Int) = CustomEdgeEffect(view, direction).apply { effect = this } fun up() = effect?.up() } class CustomEdgeEffect(private val recyclerView: RecyclerView, private val direction: Int) : EdgeEffect(recyclerView.context) { companion object { const val TAG = "CustomEdgeEffect" } private val maxEdgeNum = recyclerView.width / 4.5f private val upAnim = ValueAnimator().apply { duration = 300 interpolator = DecelerateInterpolator(2.0f) addUpdateListener { recyclerView.translationX = it.animatedValue as Float } } override fun onPull(deltaDistance: Float, displacement: Float) { super.onPull(deltaDistance, displacement) handlePull(deltaDistance) } private fun handlePull(deltaDistance: Float) { val transX = recyclerView.translationX val sign = if (direction == RecyclerView.EdgeEffectFactory.DIRECTION_RIGHT) -1 else 1 val moveX = sign * recyclerView.width * deltaDistance // 滑动阻尼 var offset = 1 - abs(transX) / maxEdgeNum if (sign < 0 && transX > 0) { offset = 1f } if (sign > 0 && transX < 0) { offset = 1f } // 1 ~ 0.1 val damping = max(offset, 0.1f) val result = transX + moveX * damping log("result $result damping $damping moveX $moveX transX $transX sign $sign") recyclerView.translationX = min(result, maxEdgeNum) } fun up() { val transX = recyclerView.translationX log("up transX $transX") if (abs(transX) > 0) { upAnim.cancel() upAnim.setFloatValues(transX, 0f) upAnim.start() } } private fun log(string: String) = Log.d(TAG, string) }
同时自定义 ItemDecoration,必要时还能对滑动方向进行控制
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.test.recent_task import android.graphics.Rect import android.view.View import androidx.recyclerview.widget.RecyclerView /** * @author liuzhen * 横向间距 */ class SpacingItemDecoration( private val firstLeftMargin: Int, private val lastRightMargin: Int, private val margin: Int ) : RecyclerView.ItemDecoration() { private var scaleNum: Int = 0 private var touchPosition: Int? = null fun updateDecoration(scaleNum: Int, touchPosition: Int) { this.scaleNum = scaleNum this.touchPosition = touchPosition } override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State ) { val position = parent.getChildAdapterPosition(view) when (position) { 0 -> outRect[firstLeftMargin, 0, margin] = 0 state.itemCount - 1 -> outRect[margin, 0, lastRightMargin] = 0 else -> outRect[margin, 0, margin] = 0 } if (touchPosition != null) { if ((touchPosition ?: 0) > position) { outRect.left += scaleNum outRect.right -= scaleNum } else if ((touchPosition ?: 0) < position) { outRect.left -= scaleNum outRect.right += scaleNum } } } fun getLeftMargin() = firstLeftMargin }
结合effect通过自定义的方式修改Decoration,达到拉伸的阻尼效果
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.test.recent_task.view import android.animation.ValueAnimator import android.annotation.SuppressLint import android.content.Context import android.graphics.Canvas import android.util.AttributeSet import android.util.Log import android.view.MotionEvent import android.view.View import android.view.animation.AccelerateDecelerateInterpolator import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import com.test.recent_task.R import com.test.recent_task.ScrollLayoutManager import com.test.recent_task.SpacingItemDecoration import com.test.recent_task.utils.DampingEdgeEffect import com.test.recent_task.utils.DragDampingHelper import kotlin.math.abs /** * @author liuzhen * 任务管理器列表 */ class RecentTaskListView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : RecyclerView(context, attrs, defStyleAttr) { companion object { const val TAG = "RecentTaskListView" } private val itemDecoration = SpacingItemDecoration( resources.getDimension(R.dimen.item_margin_left).toInt(), resources.getDimension(R.dimen.item_margin_right).toInt(), resources.getDimension(R.dimen.common_dp_28).toInt() ) private var touchView: View? = null private val scaleAnim = ValueAnimator().apply { repeatCount = 0 repeatMode = ValueAnimator.RESTART duration = 150 interpolator = AccelerateDecelerateInterpolator() addUpdateListener { animator -> val num = animator.animatedValue as Float updateDecoration(num) } } private val manager = ScrollLayoutManager(context) private var maxNum = resources.getDimension(R.dimen.common_dp_150) private var threshold = resources.getDimension(R.dimen.common_dp_50) private var dDx = 0F private var dDy = 0F // 用于列表滑动跟抬起时多次触发动效 private var isUpScale = false var removeTaskListener: ((Int) -> Unit)? = null var taskClickListener: ((Int) -> Unit)? = null var rootClickListener: (() -> Unit)? = null private val helper = ItemTouchHelper(object : ItemTouchHelper.Callback() { private var swipeBack = false private val dragDampingHelper = DragDampingHelper() override fun getMovementFlags( recyclerView: RecyclerView, viewHolder: ViewHolder ): Int { val swipeFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN return makeMovementFlags(0, swipeFlags) } override fun onMove( recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder ): Boolean { Log.d(TAG, "onMove position=${viewHolder.adapterPosition}") return false } override fun onSwiped(viewHolder: ViewHolder, direction: Int) { val position = viewHolder.adapterPosition Log.d(TAG, "onSwiped position=$position data.size=${adapter?.itemCount}") if (position >= 0) { swipeBack = false removeTaskListener?.invoke(position) } } override fun onChildDraw( c: Canvas, rv: RecyclerView, viewHolder: ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean ) { val isUp = !isCurrentlyActive val offDy = dragDampingHelper.pull(dY, isUp, 100f) swipeBack = dY > 100 setCanScroll(abs(offDy) == 0f || !isCurrentlyActive) Log.d(TAG, "onChildDraw swipeBack=$swipeBack") super.onChildDraw(c, rv, viewHolder, dX, offDy, actionState, isCurrentlyActive) } override fun convertToAbsoluteDirection(flags: Int, layoutDirection: Int): Int { return if (swipeBack) 0 else super.convertToAbsoluteDirection(flags, layoutDirection) } override fun getSwipeThreshold(viewHolder: ViewHolder) = 0.151f override fun getSwipeEscapeVelocity(defaultValue: Float) = 500F }) init { addItemDecoration(itemDecoration) helper.attachToRecyclerView(this) layoutManager = manager edgeEffectFactory = DampingEdgeEffect() post { maxNum = width / 4f } } @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(e: MotionEvent?): Boolean { touch(e) return super.onTouchEvent(e) } private fun touch(e: MotionEvent?) { Log.v(TAG, "touch action ${e?.action}") e?.apply { when (action) { MotionEvent.ACTION_DOWN -> { dDx = e.x dDy = e.y touchView = findChildViewUnder(x, y) downScale() } MotionEvent.ACTION_MOVE -> { // 正常滑动距离 val scrollX = dDx - e.x // 滑动超阈值,需要取消动效 val isHorizontalScroll = abs(scrollX) > threshold if (isHorizontalScroll && manager.canScrollHorizontally()) { upScale() } } MotionEvent.ACTION_UP -> { val isScroll = abs(dDx - e.x) < 10 && abs(dDy - e.y) < 10 log("click $touchView\ndown x=$dDx y=$dDy, up x=${e.x} y=${e.y}, isScroll $isScroll") (edgeEffectFactory as DampingEdgeEffect).up() setCanScroll(true) upScale() // 滑动时不触发点击 if (isScroll) { if (touchView == null) { rootClickListener?.invoke() } else { taskClickListener?.invoke(getChildAdapterPosition(touchView!!)) } } } } } } private fun downScale() { log("downScale") isUpScale = false scaleAnim.cancel() touchView?.let { scaleAnim.setFloatValues(1f, 0.9f) scaleAnim.start() } } private fun upScale() { log("upScale isUpScale $isUpScale") if (!isUpScale) { isUpScale = true scaleAnim.cancel() touchView?.let { scaleAnim.setFloatValues(it.scaleX, 1f) scaleAnim.start() } } } fun setCanScroll(canScroll: Boolean) { log("setCanScroll $canScroll") manager.setCanScroll(canScroll) } /** * desc:触摸时更新间距 * @param scaleNum 缩放度 */ private fun updateDecoration(scaleNum: Float) { touchView?.apply { scaleX = scaleNum scaleY = scaleNum val touchPosition = getChildAdapterPosition(this) log("updateDecoration touchPosition $touchPosition scaleNum $scaleNum") val num = (300 * (1 - scaleNum)).toInt() itemDecoration.updateDecoration(num, touchPosition) invalidateItemDecorations() } } private fun log(str: String) = Log.d(TAG, str) }