手撸一个多手势处理器,移动、缩放、旋转
2023-06-28 本文已影响0人
小风风吖
原谅我真的懒得写字了,还是把代码直接贴出来,也方便自己以后需要的时候来抄。
首先是处理器本体:
/**
* 手势帮助类,处理手势的移动、缩放、旋转,在 onTouch 事件中把 [MotionEvent] 委托给此类处理。
* @param onStart 开始手势处理,在 down 时调用,调用方应在此初始化要处理的 View 的初始状态。
* @param onEnd 本次手势处理结束,在 up 时调用,可以在此进行一些状态恢复等操作。
* @param onMove 单指移动事件,基于 [onStart] 时的相对移动位置(是累积量,不是相对上次触发的变化量)
* @param onScale 两指缩放事件,以 [onStart] 时为基准的相对缩放量(累积量,不是相对上次触发的变化量)
* @param onRotate 单指移动事件,以 [onStart] 时为基准的相对旋转角度(累积量,不是相对上次触发的变化量)
*/
class GestureHelper(
var onStart: (() -> Unit)? = null,
var onEnd: (() -> Unit)? = null,
var onMove: ((Float, Float) -> Unit)? = null,
var onScale: ((Float) -> Unit)? = null,
var onRotate: ((Float) -> Unit)? = null
) {
private val moveHandler = MoveHandler { x, y ->
this.onMove?.invoke(x, y)
}
private val scaleHandler = ScaleHandler(
onScale = {
this.onScale?.invoke(it)
},
onRotate = {
this.onRotate?.invoke(it)
}
)
fun onTouch(event: MotionEvent) {
if (event.actionMasked == MotionEvent.ACTION_POINTER_DOWN && event.pointerCount == 2 && !scaleHandler.isStart) {
scaleHandler.pointDown(1, event.getX(1), event.getY(1))
}
when (event.action) {
MotionEvent.ACTION_DOWN -> {
onStart?.invoke()
moveHandler.pointDown(event.x, event.y)
scaleHandler.pointDown(0, event.x, event.y)
}
MotionEvent.ACTION_MOVE -> {
if (event.pointerCount == 1 && !scaleHandler.isStart) {
moveHandler.handleMove(event.x, event.y)
}
if (event.pointerCount == 2) {
scaleHandler.dispatch(event.getX(0), event.getY(0), event.getX(1), event.getY(1))
}
}
MotionEvent.ACTION_UP -> {
scaleHandler.isStart = false
onEnd?.invoke()
}
}
}
}
本着面向对象的原则,把单指和两指的后续处理分别交给对应的接收器。
单指移动处理:
/**
* 处理移动事件
* @param onMove 移动回调,入参是相对 DOWN 事件的偏移量
*/
class MoveHandler(val onMove: (Float, Float) -> Unit) {
private val downPoint = PointF(0f, 0f)
private var startMove = false
fun pointDown(x: Float, y: Float) {
downPoint.x = x
downPoint.y = y
}
fun handleMove(x: Float, y: Float) {
if (startMove) {
onMove(x - downPoint.x, y - downPoint.y)
} else {
if (max(abs(x - downPoint.x), abs(y - downPoint.y)) > 25)
startMove = true
}
}
}
两指缩放和旋转:
/**
* 处理缩放事件
* @param onScale 缩放回调,相对 DOWN 事件的缩放比例
* @param onRotate 旋转回调,相对 DOWN 事件的旋转角度
*/
class ScaleHandler(val onScale: (Float) -> Unit, val onRotate: (Float) -> Unit) {
// x1, y1, x2, y2
private val downPoints = arrayOf(0f, 0f, 0f, 0f)
private var startScale = false
private var startRotate = false
var isStart: Boolean
get() = startScale || startRotate
set(value) {
if (!value) {
startScale = false
startRotate = false
}
}
fun pointDown(index: Int, x: Float, y: Float) {
if (startScale || startRotate) return
if (index == 0) {
downPoints[0] = x
downPoints[1] = y
}
if (index == 1) {
downPoints[2] = x
downPoints[3] = y
}
}
fun dispatch(x1: Float, y1: Float, x2: Float, y2: Float) {
if (handleRotate(x1, y1, x2, y2)) return
if (handleScale(x1, y1, x2, y2)) return
}
private fun handleScale(x1: Float, y1: Float, x2: Float, y2: Float): Boolean {
if (startScale) onScale(calcScale(x1, y1, x2, y2))
else if (!isStart) {
val scale = calcScale(x1, y1, x2, y2)
if (abs(scale - 1) > 0.06f) {
startScale = true
onScale(scale)
}
}
return startScale
}
private fun handleRotate(x1: Float, y1: Float, x2: Float, y2: Float): Boolean {
if (startRotate) onRotate(calcRotate(x1, y1, x2, y2))
else if (!isStart) {
val rotation = calcRotate(x1, y1, x2, y2)
if (abs(rotation) > 6f) {
startRotate = true
onRotate(rotation)
}
}
return startRotate
}
private fun calcScale(x1: Float, y1: Float, x2: Float, y2: Float): Float {
val downLength = GraphUtil.calcLength(GraphVector(downPoints[0], downPoints[1], downPoints[2], downPoints[3]))
val nowLength = GraphUtil.calcLength(GraphVector(x1, y1, x2, y2))
return nowLength / downLength
}
private fun calcRotate(x1: Float, y1: Float, x2: Float, y2: Float): Float {
return GraphUtil.calcVectorDegree(
GraphVector(downPoints[0], downPoints[1], downPoints[2], downPoints[3]),
GraphVector(x1, y1, x2, y2)
)
}
}
下面是重点了,一些二维向量的相关计算:
data class GraphVector(
val x1: Float,
val y1: Float,
val x2: Float,
val y2: Float,
)
object GraphUtil {
/**
* 计算两点间距(向量模)
*/
fun calcLength(v: GraphVector) = hypot(v.x2 - v.x1.toDouble(), v.y2 - v.y1.toDouble()).toFloat()
/**
* 两个向量点积
*/
fun calcDotProduct(a: GraphVector, b: GraphVector): Float {
val ax = a.x2 - a.x1
val ay = a.y2 - a.y1
val bx = b.x2 - b.x1
val by = b.y2 - b.y1
return ax * bx + ay * by
}
/**
* 两个向量叉积
*/
fun calcCrossProduct(a: GraphVector, b: GraphVector): Float {
val ax = a.x2 - a.x1
val ay = a.y2 - a.y1
val bx = b.x2 - b.x1
val by = b.y2 - b.y1
return ax * by - bx * ay
}
/**
* 计算两个向量夹角,有符号
* 公式:A×B = |A|·|B|·Cos(Θ) 两向量点积等于两向量模与夹角余弦值的乘积
* @return 两个向量夹角 -180~180
*/
fun calcVectorDegree(a: GraphVector, b: GraphVector): Float {
val degreeAbs = calcVectorDegreeAbs(a, b)
val crossProduct = calcCrossProduct(a, b)
return if (crossProduct > 0) degreeAbs else -degreeAbs
}
/**
* 计算两个向量绝对夹角,无符号
* 公式:A×B = |A|·|B|·Cos(Θ) 两向量点积等于两向量模与夹角余弦值的乘积
* @return 两个向量所在直线的夹角 0~180,需要结合叉积另行判断正负
*/
private fun calcVectorDegreeAbs(a: GraphVector, b: GraphVector): Float {
val dotProduct = calcDotProduct(a, b)
val aLength = calcLength(a)
val bLength = calcLength(b)
return Math.toDegrees(acos(dotProduct.toDouble() / (aLength * bLength))).toFloat()
}
}
最后在贴一个使用样例:
/**
* 手势拖动、缩放、旋转样例
*/
class TestMatrixFrag : Fragment(R.layout.fragment_test_matrix) {
private val vb by viewBinding(FragmentTestMatrixBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
}
// transX, transY, scale, rotation,记录开始处理手势时的 View 状态
private var startParams = arrayOf(0f, 0f, 0f, 0f)
private val gestureHelper = GestureHelper(
onStart = {
startParams[0] = vb.viewTarget.translationX
startParams[1] = vb.viewTarget.translationY
startParams[2] = vb.viewTarget.scaleX
startParams[3] = vb.viewTarget.rotation
},
onMove = { x, y ->
vb.viewTarget.translationX = startParams[0] + x
vb.viewTarget.translationY = startParams[1] + y
},
onScale = {
vb.viewTarget.scaleX = startParams[2] * it
vb.viewTarget.scaleY = startParams[2] * it
},
onRotate = {
vb.viewTarget.rotation = startParams[3] + it
}
)
@SuppressLint("ClickableViewAccessibility")
private fun initView() = with(vb) {
viewMark.background = GradientDrawable().also {
it.setStroke(10, (0xFF0057B3).toInt())
}
viewTarget.setBackgroundColor((0x59FF5A5A).toInt())
// 重点在这里,设置 OnTouchListener 然后把 MotionEvent 交给 GestureHelper 处理
viewMark.setOnTouchListener { _, event ->
gestureHelper.onTouch(event)
true
}
}
}