原諒我真的懶得寫字了,還是把代碼直接貼出來,也方便自己以后需要的時候來抄。
首先是處理器本體:
/**
* 手勢幫助類,處理手勢的移動、縮放、旋轉(zhuǎn),在 onTouch 事件中把 [MotionEvent] 委托給此類處理。
* @param onStart 開始手勢處理,在 down 時調(diào)用,調(diào)用方應在此初始化要處理的 View 的初始狀態(tài)。
* @param onEnd 本次手勢處理結(jié)束,在 up 時調(diào)用,可以在此進行一些狀態(tài)恢復等操作。
* @param onMove 單指移動事件,基于 [onStart] 時的相對移動位置(是累積量,不是相對上次觸發(fā)的變化量)
* @param onScale 兩指縮放事件,以 [onStart] 時為基準的相對縮放量(累積量,不是相對上次觸發(fā)的變化量)
* @param onRotate 單指移動事件,以 [onStart] 時為基準的相對旋轉(zhuǎn)角度(累積量,不是相對上次觸發(fā)的變化量)
*/
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()
}
}
}
}
本著面向?qū)ο蟮脑瓌t,把單指和兩指的后續(xù)處理分別交給對應的接收器。
單指移動處理:
/**
* 處理移動事件
* @param onMove 移動回調(diào),入?yún)⑹窍鄬?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
}
}
}
兩指縮放和旋轉(zhuǎn):
/**
* 處理縮放事件
* @param onScale 縮放回調(diào),相對 DOWN 事件的縮放比例
* @param onRotate 旋轉(zhuǎn)回調(diào),相對 DOWN 事件的旋轉(zhuǎn)角度
*/
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)
)
}
}
下面是重點了,一些二維向量的相關(guān)計算:
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,需要結(jié)合叉積另行判斷正負
*/
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()
}
}
最后在貼一個使用樣例:
/**
* 手勢拖動、縮放、旋轉(zhuǎn)樣例
*/
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 狀態(tài)
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())
// 重點在這里,設(shè)置 OnTouchListener 然后把 MotionEvent 交給 GestureHelper 處理
viewMark.setOnTouchListener { _, event ->
gestureHelper.onTouch(event)
true
}
}
}