Android 清屏功能实现

向左滑动时, 屏幕中的 view 跟随手指滑动, 滑动到一定距离或者有超过指定的滑动速度, 松开时, 触发滑动动画, 清屏或者还原视图.

主要涉及到 onTouchEvent 的处理, 和 属性动画 的启动.

如果业务有有需要不跟随滑动的 view, 可以设置 白名单 -> whitelist, 还有一些具体的 业务 view 需要再滑动处理自己的事件时, 直接在自己的 onTouchEvent 拦截即可.

另外需要注意动画需要在 onDetachedFromWindow 时回收, 不会会造成内存泄漏.

具体代码如下

package com.pb.test.bilibili.widget

import android.animation.Animator
import android.animation.ObjectAnimator
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.VelocityTracker
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.forEach
import androidx.core.view.forEachIndexed
import com.pb.test.bilibili.BiliLogger
import com.pb.test.bilibili.logDebug
import com.pb.test.utils.UITools
import kotlin.math.abs

/**
 *
 * author : YingYing Zhang
 * e-mail : 540108843@qq.com
 * time   : 2022-03-20
 * desc   :
 *
 */
class LiveRoomRootView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr), BiliLogger {
    private var startX = 0F
    private var endX = 0F
    private var delta = 0F
    private var speedX = 0F
    private val screenWidth = UITools.getScreenWidth(context)
    private val MIN_DISTANCE = screenWidth / 3
    private val MIN_SPEED = 400

    // 不移动名单
    val whitelist = mutableListOf<Int>()

    // 执行自己的 onTouchEvent 名单
    val interceptList = ArrayList<Int>()

    // 是否处于清屏状态
    private var isCleared = false
    private val ANIMATOR_DURATION = 500L
    private var leftDuration = 0L
    private var leftDistance = 0F

    private val animator = ObjectAnimator.ofFloat(1F, 0F).apply {
        duration = ANIMATOR_DURATION
        addUpdateListener { animation ->
            // 1 - 0
            val percent = animation?.animatedValue as Float
            logDebug { "animator - update, percent = $percent" }

            if (isCleared) {
                if (isValidAction()) {
                    val ratio = leftDistance * percent
                    logDebug { "animator - update - recover - valid UP, radio = $ratio" }
                    executeSlideWithFinger(ratio)
                } else {
                    val ratio = screenWidth - abs(delta) * percent
                    logDebug { "animator - update  - recover - invalid UP, radio = $ratio" }
                    executeSlideWithFinger(ratio)
                }
            } else {
                if (isValidAction()) {
                    val ratio = screenWidth - leftDistance * percent
                    logDebug { "animator - update - clear - valid UP, radio = $ratio" }
                    executeSlideWithFinger(ratio)
                } else {
                    val ratio = abs(delta) * percent
                    logDebug { "animator - update  - clear - invalid UP, radio = $ratio" }
                    executeSlideWithFinger(ratio)
                }
            }
        }
        addListener(object : Animator.AnimatorListener {
            override fun onAnimationStart(animation: Animator?) {}

            override fun onAnimationEnd(animation: Animator?) {
                if (isCleared) {
                    if (isValidAction()) {
                        logDebug { "animator - end - isClear false" }
                        isCleared = false
                    }
                } else {
                    if (isValidAction()) {
                        logDebug { "animator - end - isClear true" }
                        isCleared = true
                    }
                }
                resetResources()
            }

            override fun onAnimationCancel(animation: Animator?) {}

            override fun onAnimationRepeat(animation: Animator?) {}
        })
    }

    private val velocityTracker: VelocityTracker by lazy(LazyThreadSafetyMode.NONE) {
        VelocityTracker.obtain()
    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        return super.dispatchTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        if (animator.isRunning) return true
        logDebug { "onTouchEvent, event action = ${event?.action}" }
        velocityTracker.addMovement(event)
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                logDebug { "onTouchEvent-ACTION_DOWN, action = ACTION_DOWN" }
                startX = event.rawX
            }
            MotionEvent.ACTION_MOVE -> {
                logDebug { "onTouchEvent-ACTION_MOVE, action = ACTION_MOVE" }
                // 滑动不能超过屏幕的宽度
                val delta = event.rawX - startX

                logDebug { "onTouchEvent-ACTION_MOVE, isCleared = $isCleared" }
                if (isCleared) {
                    // 处于清屏状态, 准备取消清屏
                    if (delta < 0 && abs(delta) <= screenWidth) {
                        val tempDelta = delta + screenWidth
                        executeSlideWithFinger(tempDelta)
                    } else {
                        logDebug { "onTouchEvent-ACTION_MOVE, already in cleared, can't left" }
                    }
                } else {
                    // 准备去清屏
                    if (delta > 0 && delta <= screenWidth) {
                        executeSlideWithFinger(delta)
                    } else {
                        logDebug { "onTouchEvent-ACTION_MOVE, can't right" }
                    }
                }
            }
            MotionEvent.ACTION_UP -> {
                logDebug { "onTouchEvent-ACTION_UP, action = ACTION_UP" }
                // 1秒被滑过多少个像素
                velocityTracker.computeCurrentVelocity(100)
                speedX = velocityTracker.xVelocity
                logDebug { "onTouchEvent-ACTION_UP, 1秒内划过 x = $speedX, y = ${velocityTracker.yVelocity}" }
                // 设置了 maxVelocity 最大就显示成这个值
                // velocityTracker.computeCurrentVelocity(1000, 150F)

                endX = event.rawX
                delta = endX - startX
                logDebug { "onTouchEvent-ACTION_UP, startX = $startX, endX = $endX, delta = $delta" }

                // 处于清屏状态, 不能左滑
                if (isCleared && delta > 0) return super.onTouchEvent(event)
                // 非清屏状态, 不能右滑
                if (!isCleared && delta < 0) return super.onTouchEvent(event)

                /**
                 * duration = left / width * duration
                 *
                 * delta > 0 && isClear = false -> 当前是非清屏状态, 右滑
                 *  1. delta >= MIN_DISTANCE, 滑动有效, 去清屏 --> 右滑, 距离: width - delta
                 *  2. delta < MIN_DISTANCE, 滑动无效, 还原非清屏状态 --> 左滑, 距离: delta
                 *
                 * delta < 0 && isClear = true -> 当前是清醒状态, 左滑
                 *  1. abs(delta) >= MIN_DISTANCE, 滑动有效, 还原非清屏状态 --> 左滑, 距离: width - abs(delta)
                 *  2. abs(delta) < MIN_DISTANCE, 滑动无效, 仍是清屏状态 --> 右滑, 距离: abs(delta)
                 */
                if (isValidAction()) {
                    leftDistance = screenWidth - abs(delta)
                    leftDuration = (leftDistance * ANIMATOR_DURATION / screenWidth).toLong()
                    logDebug { "onTouchEvent-ACTION_UP - valid, leftDistance = $leftDistance, leftDuration = $leftDuration" }
                    animator.duration = leftDuration
                } else {
                    leftDistance = abs(delta)
                    leftDuration = (leftDistance * ANIMATOR_DURATION / screenWidth).toLong()
                    logDebug { "onTouchEvent-ACTION_UP - invalid UP, leftDistance = $leftDistance, leftDuration = $leftDuration" }
                    animator.duration = leftDuration
                }
                animator.start()
            }
            MotionEvent.ACTION_CANCEL -> {
                logDebug { "onTouchEvent-ACTION_CANCEL" }
                velocityTracker.recycle()
            }
        }

        return true
    }

    // 跟随手指滑动
    private fun executeSlideWithFinger(delta: Float) {
        logDebug { "slideWithFinger, delta = $delta" }
        forEachIndexed { index, view ->
            if (whitelist.contains(view.id)) {
                logDebug { "slideWithFinger, whitelist contains ${view.id}" }
                return@forEachIndexed
            }
            view.translationX = delta
        }
    }

    private fun isValidAction() = speedX > MIN_SPEED || abs(delta) >= MIN_DISTANCE

    private fun resetResources() {
        logDebug { "resetResources" }
        delta = 0F
        speedX = 0F
        startX = 0F
        endX = 0F
        leftDistance = 0F
        leftDuration = 0L
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        return super.onInterceptTouchEvent(ev)
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        velocityTracker.recycle()
        animator.cancel()
        animator.removeAllUpdateListeners()
        animator.removeAllListeners()
    }

    override val logTag = "LiveRoomRootView"
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吃饱很舒服

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值