向左滑动时, 屏幕中的 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"
}