本文將手把手教你創建一個支持拖動、縮放、旋轉等多種手勢交互的自定義 View,并提供完整的代碼實現和優化建議。
一、基礎實現
1.1 創建自定義 View 骨架
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.*class InteractiveView @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {// 繪制相關private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {color = Color.BLUEstyle = Paint.Style.FILL}private var circleRadius = 100fprivate val originalRect = RectF()// 變換參數private var offsetX = 0fprivate var offsetY = 0fprivate var scaleFactor = 1fprivate var rotationAngle = 0f// 手勢檢測器private val gestureDetector: GestureDetectorprivate val scaleDetector: ScaleGestureDetectorprivate val rotationDetector: RotationGestureDetectorinit {// 初始化手勢檢測器gestureDetector = GestureDetector(context, GestureListener())scaleDetector = ScaleGestureDetector(context, ScaleListener())rotationDetector = RotationGestureDetector(context, RotationListener())}override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)originalRect.set(0f, 0f, w.toFloat(), h.toFloat())}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)canvas.save()// 應用變換canvas.translate(offsetX, offsetY)canvas.scale(scaleFactor, scaleFactor, pivotX, pivotY)canvas.rotate(rotationAngle, pivotX, pivotY)// 繪制內容canvas.drawCircle(originalRect.centerX(),originalRect.centerY(),circleRadius,paint)canvas.restore()}override fun onTouchEvent(event: MotionEvent): Boolean {scaleDetector.onTouchEvent(event)rotationDetector.onTouchEvent(event)gestureDetector.onTouchEvent(event)return true}// 其他實現將在下文展開...
}
1.2 實現基本手勢
拖動處理:
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {override fun onScroll(e1: MotionEvent,e2: MotionEvent,distanceX: Float,distanceY: Float): Boolean {offsetX -= distanceXoffsetY -= distanceYapplyBoundaryConstraints()invalidate()return true}override fun onDoubleTap(e: MotionEvent): Boolean {// 雙擊重置變換resetTransformations()invalidate()return true}
}private fun resetTransformations() {offsetX = 0foffsetY = 0fscaleFactor = 1frotationAngle = 0f
}
縮放處理:
private var pivotX = 0f
private var pivotY = 0fprivate inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {pivotX = detector.focusXpivotY = detector.focusYreturn true}override fun onScale(detector: ScaleGestureDetector): Boolean {val scale = detector.scaleFactorscaleFactor *= scalescaleFactor = scaleFactor.coerceIn(0.1f, 5f)// 調整中心點偏移offsetX += (pivotX - offsetX) * (1 - scale)offsetY += (pivotY - offsetY) * (1 - scale)invalidate()return true}
}
二、高級手勢實現
2.1 旋轉手勢檢測器
class RotationGestureDetector(context: Context,private val listener: OnRotationGestureListener
) {private var prevAngle = 0ffun onTouchEvent(event: MotionEvent): Boolean {if (event.pointerCount != 2) return falsewhen (event.actionMasked) {MotionEvent.ACTION_POINTER_DOWN -> {prevAngle = getAngle(event)}MotionEvent.ACTION_MOVE -> {val newAngle = getAngle(event)listener.onRotate(newAngle - prevAngle)prevAngle = newAngle}}return true}private fun getAngle(event: MotionEvent): Float {val dx = event.getX(0) - event.getX(1)val dy = event.getY(0) - event.getY(1)return Math.toDegrees(atan2(dy.toDouble(), dx.toDouble())).toFloat()}interface OnRotationGestureListener {fun onRotate(angleDelta: Float)}
}// 在 InteractiveView 中添加:
private inner class RotationListener : RotationGestureDetector.OnRotationGestureListener {override fun onRotate(angleDelta: Float) {rotationAngle += angleDeltarotationAngle %= 360invalidate()}
}
2.2 邊界約束
private fun applyBoundaryConstraints() {val scaledWidth = originalRect.width() * scaleFactorval scaledHeight = originalRect.height() * scaleFactorval maxOffsetX = (scaledWidth - originalRect.width()) / 2val maxOffsetY = (scaledHeight - originalRect.height()) / 2offsetX = offsetX.coerceIn(-maxOffsetX, maxOffsetX)offsetY = offsetY.coerceIn(-maxOffsetY, maxOffsetY)
}
三、性能優化實現
慣性滑動
private val scroller = Scroller(context)override fun computeScroll() {if (scroller.computeScrollOffset()) {offsetX = scroller.currX.toFloat()offsetY = scroller.currY.toFloat()applyBoundaryConstraints()invalidate()}
}private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {override fun onFling(e1: MotionEvent,e2: MotionEvent,velocityX: Float,velocityY: Float): Boolean {scroller.fling(offsetX.toInt(),offsetY.toInt(),velocityX.toInt(),velocityY.toInt(),Int.MIN_VALUE,Int.MAX_VALUE,Int.MIN_VALUE,Int.MAX_VALUE)invalidate()return true}
}
四、完整布局示例
<com.example.app.InteractiveViewandroid:id="@+id/interactiveView"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#F0F0F0"/>
五、最佳實踐建議
-
繪制優化:
override fun onDraw(canvas: Canvas) {// 避免在繪制過程中創建新對象canvas.drawCircle(originalRect.centerX(),originalRect.centerY(),circleRadius,paint // 重用預定義的 Paint 對象) }
-
手勢優先級處理:
override fun onTouchEvent(event: MotionEvent): Boolean {when {scaleDetector.isInProgress -> scaleDetector.onTouchEvent(event)rotationDetector.isInProgress -> rotationDetector.onTouchEvent(event)else -> gestureDetector.onTouchEvent(event)}return true }
-
多指觸控處理:
private var activePointerId = MotionEvent.INVALID_POINTER_IDoverride fun onTouchEvent(event: MotionEvent): Boolean {when (event.actionMasked) {MotionEvent.ACTION_POINTER_DOWN -> {activePointerId = event.getPointerId(event.actionIndex)}MotionEvent.ACTION_POINTER_UP -> {val remainingPointer = if (event.actionIndex == 0) 1 else 0activePointerId = event.getPointerId(remainingPointer)}}// 其他手勢處理... }
六、完整功能集成
最終的自定義 View 將支持:
- 單指拖動
- 雙指縮放(帶錨點)
- 雙指旋轉
- 慣性滑動
- 雙擊重置
- 邊界約束
- 多指觸控支持
可以通過組合這些手勢實現復雜的交互效果,如圖片查看器、可操作圖表等。
以下是為您整合所有功能的完整實現代碼:
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.*
import android.widget.OverScroller
import kotlin.math.*class AdvancedInteractiveView @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {// 繪制參數private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {color = Color.BLUEstyle = Paint.Style.FILL}private var circleRadius = 100fprivate val originalRect = RectF()// 變換參數private var offsetX = 0fprivate var offsetY = 0fprivate var scaleFactor = 1fprivate var rotationAngle = 0fprivate var pivotX = 0fprivate var pivotY = 0f// 手勢檢測器private val gestureDetector: GestureDetectorprivate val scaleDetector: ScaleGestureDetectorprivate val rotationDetector: RotationGestureDetectorprivate val scroller = OverScroller(context)// 邊界約束參數private var minScale = 0.5fprivate var maxScale = 5fprivate var isScaling = falseprivate var activePointerId = MotionEvent.INVALID_POINTER_IDinit {gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {override fun onScroll(e1: MotionEvent,e2: MotionEvent,distanceX: Float,distanceY: Float): Boolean {if (!isScaling) {offsetX -= distanceXoffsetY -= distanceYapplyBoundaryConstraints()invalidate()}return true}override fun onDoubleTap(e: MotionEvent): Boolean {resetTransformations()invalidate()return true}override fun onFling(e1: MotionEvent,e2: MotionEvent,velocityX: Float,velocityY: Float): Boolean {scroller.fling(offsetX.toInt(),offsetY.toInt(),velocityX.toInt(),velocityY.toInt(),Int.MIN_VALUE,Int.MAX_VALUE,Int.MIN_VALUE,Int.MAX_VALUE,100,100)invalidate()return true}})scaleDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {isScaling = truepivotX = detector.focusXpivotY = detector.focusYreturn true}override fun onScale(detector: ScaleGestureDetector): Boolean {val scale = detector.scaleFactorval newScale = scaleFactor * scaleif (newScale in minScale..maxScale) {// 調整偏移量保持錨點位置offsetX += (pivotX - offsetX) * (1 - scale)offsetY += (pivotY - offsetY) * (1 - scale)scaleFactor = newScale}applyBoundaryConstraints()invalidate()return true}override fun onScaleEnd(detector: ScaleGestureDetector) {isScaling = false}})rotationDetector = RotationGestureDetector(object : RotationGestureDetector.OnRotationGestureListener {override fun onRotate(angleDelta: Float) {rotationAngle += angleDeltarotationAngle %= 360invalidate()}})}override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)originalRect.set(0f, 0f, w.toFloat(), h.toFloat())resetTransformations()}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)canvas.save()// 應用變換矩陣canvas.translate(offsetX, offsetY)canvas.scale(scaleFactor, scaleFactor, pivotX, pivotY)canvas.rotate(rotationAngle, pivotX, pivotY)// 繪制圓形canvas.drawCircle(originalRect.centerX(),originalRect.centerY(),circleRadius,paint)canvas.restore()}override fun computeScroll() {if (scroller.computeScrollOffset()) {offsetX = scroller.currX.toFloat()offsetY = scroller.currY.toFloat()applyBoundaryConstraints()invalidate()}}override fun onTouchEvent(event: MotionEvent): Boolean {scaleDetector.onTouchEvent(event)gestureDetector.onTouchEvent(event)rotationDetector.onTouchEvent(event)handleMultiTouch(event)return true}private fun handleMultiTouch(event: MotionEvent) {when (event.actionMasked) {MotionEvent.ACTION_POINTER_DOWN -> {activePointerId = event.getPointerId(event.actionIndex)}MotionEvent.ACTION_POINTER_UP -> {val remainingPointer = if (event.actionIndex == 0) 1 else 0activePointerId = event.getPointerId(remainingPointer)}}}private fun applyBoundaryConstraints() {val viewWidth = originalRect.width()val viewHeight = originalRect.height()val scaledWidth = viewWidth * scaleFactorval scaledHeight = viewHeight * scaleFactorval maxOffsetX = (scaledWidth - viewWidth) / 2val maxOffsetY = (scaledHeight - viewHeight) / 2offsetX = offsetX.coerceIn(-maxOffsetX, maxOffsetX)offsetY = offsetY.coerceIn(-maxOffsetY, maxOffsetY)}private fun resetTransformations() {offsetX = 0foffsetY = 0fscaleFactor = 1frotationAngle = 0fpivotX = originalRect.centerX()pivotY = originalRect.centerY()invalidate()}// 自定義旋轉手勢檢測器private class RotationGestureDetector(private val listener: OnRotationGestureListener) {private var prevAngle = 0ffun onTouchEvent(event: MotionEvent): Boolean {if (event.pointerCount != 2) return falsewhen (event.actionMasked) {MotionEvent.ACTION_POINTER_DOWN -> prevAngle = getAngle(event)MotionEvent.ACTION_MOVE -> {val newAngle = getAngle(event)listener.onRotate(newAngle - prevAngle)prevAngle = newAngle}}return true}private fun getAngle(event: MotionEvent): Float {val dx = event.getX(0) - event.getX(1)val dy = event.getY(0) - event.getY(1)return Math.toDegrees(atan2(dy.toDouble(), dx.toDouble())).toFloat()}interface OnRotationGestureListener {fun onRotate(angleDelta: Float)}}
}
功能實現說明
-
單指拖動
- 通過
GestureDetector
檢測滾動事件 - 在
onScroll
中更新offsetX/Y
值 - 添加邊界約束防止移出可視區域
- 通過
-
雙指縮放(帶錨點)
- 使用
ScaleGestureDetector
檢測縮放手勢 - 記錄縮放錨點
(pivotX, pivotY)
- 動態調整偏移量保持錨點位置穩定
- 限制縮放范圍(0.5-5倍)
- 使用
-
雙指旋轉
- 自定義
RotationGestureDetector
計算旋轉角度 - 通過兩點坐標計算旋轉角度差值
- 更新
rotationAngle
并限制在0-360度之間
- 自定義
-
慣性滑動
- 使用
OverScroller
實現流暢的慣性滑動 - 在
onFling
中初始化滑動參數 - 在
computeScroll
中持續更新位置
- 使用
-
雙擊重置
- 在
onDoubleTap
中重置所有變換參數 - 重置位置、縮放、旋轉到初始狀態
- 在
-
邊界約束
applyBoundaryConstraints
方法計算最大偏移量- 根據當前縮放比例動態調整邊界限制
- 在每次位置變化后調用約束方法
-
多指觸控支持
- 處理
ACTION_POINTER_DOWN/UP
事件 - 跟蹤活動指針ID
- 正確處理多指手勢的切換
- 處理
使用方式
- 在XML布局中添加:
<com.your.package.AdvancedInteractiveViewandroid:layout_width="match_parent"android:layout_height="match_parent"android:background="#F0F0F0"/>
- 自定義屬性建議(可選):
<resources><declare-styleable name="AdvancedInteractiveView"><attr name="minScale" format="float" /><attr name="maxScale" format="float" /><attr name="shapeColor" format="color" /></declare-styleable>
</resources>
性能優化建議
- 硬件加速:
<application android:hardwareAccelerated="true">
- 繪制優化:
- 避免在onDraw中創建新對象
- 使用
canvas.saveLayer()
替代多次繪制 - 對于復雜圖形使用Bitmap緩存
- 手勢優化:
- 設置合適的手勢檢測閾值
- 使用
ViewConfiguration
獲取系統標準值:
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
- 內存管理:
- 在onDetachedFromWindow中釋放資源
- 使用弱引用持有上下文
可根據需要調整以下參數:
circleRadius
:初始圓形半徑minScale/maxScale
:縮放范圍限制- 顏色和樣式通過Paint對象自定義
- 邊界約束計算邏輯調整
實際使用時可擴展以下功能:
- 添加更多圖形元素
- 實現手勢沖突解決策略
- 添加觸摸反饋動畫
- 支持更多手勢(如長按、快速滑動等)