本文将手把手教你创建一个支持拖动、缩放、旋转等多种手势交互的自定义 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对象自定义
- 边界约束计算逻辑调整
实际使用时可扩展以下功能:
- 添加更多图形元素
- 实现手势冲突解决策略
- 添加触摸反馈动画
- 支持更多手势(如长按、快速滑动等)