随笔 - 129,  文章 - 3,  评论 - 50,  阅读 - 15万

在 recyclerView 列表中,滑动到边界后,继续滑动,会发现自带一个阻尼效果,但是往往不能满足产品需求,需要自定义

比如拉伸的最大距离,或者拉伸的位置

模仿安卓最近任务列表,列表中item可以上下左右滑动,并且左右下方向滑动到边界后会产生阻尼效果,随着拉伸的距离增大而增大

方案1:这里可以自定义阻尼算法,比如粗糙的方式计算

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)

}
DragDampingHelper

缺点很明显,如果只是单方向滑动,没什么问题,但是如果按下后来回滑动反人类操作,适配难度较大,这里是直接归零处理,只有往一个方向时才开始计算阻尼

方案2:使用 recyclerView 自带的 EdgeEffect

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)

}
DampingEdgeEffect

同时自定义 ItemDecoration,必要时还能对滑动方向进行控制

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

}
SpacingItemDecoration

结合effect通过自定义的方式修改Decoration,达到拉伸的阻尼效果

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)

}
RecentTaskListView

 

posted on   翻滚的咸鱼  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 本地部署 DeepSeek:小白也能轻松搞定!
· 如何给本地部署的DeepSeek投喂数据,让他更懂你
· 从 Windows Forms 到微服务的经验教训
· 李飞飞的50美金比肩DeepSeek把CEO忽悠瘸了,倒霉的却是程序员
· 超详细,DeepSeek 接入PyCharm实现AI编程!(支持本地部署DeepSeek及官方Dee
历史上的今天:
2023-01-15 Recyclerview、Viewpager 实现视频自动播放方案
< 2025年2月 >
26 27 28 29 30 31 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 1
2 3 4 5 6 7 8

点击右上角即可分享
微信分享提示