本文将手把手教你创建一个支持拖动、缩放、旋转等多种手势交互的自定义 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.BLUE
style = Paint.Style.FILL
}
private var circleRadius = 100f
private val originalRect = RectF()
// 变换参数
private var offsetX = 0f
private var offsetY = 0f
private var scaleFactor = 1f
private var rotationAngle = 0f
// 手势检测器
private val gestureDetector: GestureDetector
private val scaleDetector: ScaleGestureDetector
private val rotationDetector: RotationGestureDetector
init {
// 初始化手势检测器
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 -= distanceX
offsetY -= distanceY
applyBoundaryConstraints()
invalidate()
return true
}
override fun onDoubleTap(e: MotionEvent): Boolean {
// 双击重置变换
resetTransformations()
invalidate()
return true
}
}
private fun resetTransformations() {
offsetX = 0f
offsetY = 0f
scaleFactor = 1f
rotationAngle = 0f
}
缩放处理:
private var pivotX = 0f
private var pivotY = 0f
private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
pivotX = detector.focusX
pivotY = detector.focusY
return true
}
override fun onScale(detector: ScaleGestureDetector): Boolean {
val scale = detector.scaleFactor
scaleFactor *= scale
scaleFactor = 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 = 0f
fun onTouchEvent(event: MotionEvent): Boolean {
if (event.pointerCount != 2) return false
when (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 += angleDelta
rotationAngle %= 360
invalidate()
}
}
2.2 边界约束
private fun applyBoundaryConstraints() {
val scaledWidth = originalRect.width() * scaleFactor
val scaledHeight = originalRect.height() * scaleFactor
val maxOffsetX = (scaledWidth - originalRect.width()) / 2
val maxOffsetY = (scaledHeight - originalRect.height()) / 2
offsetX = 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.InteractiveView
android: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_ID override 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 0 activePointerId = 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.BLUE
style = Paint.Style.FILL
}
private var circleRadius = 100f
private val originalRect = RectF()
// 变换参数
private var offsetX = 0f
private var offsetY = 0f
private var scaleFactor = 1f
private var rotationAngle = 0f
private var pivotX = 0f
private var pivotY = 0f
// 手势检测器
private val gestureDetector: GestureDetector
private val scaleDetector: ScaleGestureDetector
private val rotationDetector: RotationGestureDetector
private val scroller = OverScroller(context)
// 边界约束参数
private var minScale = 0.5f
private var maxScale = 5f
private var isScaling = false
private var activePointerId = MotionEvent.INVALID_POINTER_ID
init {
gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (!isScaling) {
offsetX -= distanceX
offsetY -= distanceY
applyBoundaryConstraints()
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 = true
pivotX = detector.focusX
pivotY = detector.focusY
return true
}
override fun onScale(detector: ScaleGestureDetector): Boolean {
val scale = detector.scaleFactor
val newScale = scaleFactor * scale
if (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 += angleDelta
rotationAngle %= 360
invalidate()
}
})
}
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 0
activePointerId = event.getPointerId(remainingPointer)
}
}
}
private fun applyBoundaryConstraints() {
val viewWidth = originalRect.width()
val viewHeight = originalRect.height()
val scaledWidth = viewWidth * scaleFactor
val scaledHeight = viewHeight * scaleFactor
val maxOffsetX = (scaledWidth - viewWidth) / 2
val maxOffsetY = (scaledHeight - viewHeight) / 2
offsetX = offsetX.coerceIn(-maxOffsetX, maxOffsetX)
offsetY = offsetY.coerceIn(-maxOffsetY, maxOffsetY)
}
private fun resetTransformations() {
offsetX = 0f
offsetY = 0f
scaleFactor = 1f
rotationAngle = 0f
pivotX = originalRect.centerX()
pivotY = originalRect.centerY()
invalidate()
}
// 自定义旋转手势检测器
private class RotationGestureDetector(
private val listener: OnRotationGestureListener
) {
private var prevAngle = 0f
fun onTouchEvent(event: MotionEvent): Boolean {
if (event.pointerCount != 2) return false
when (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.AdvancedInteractiveView
android: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对象自定义
- 边界约束计算逻辑调整
实际使用时可扩展以下功能:
- 添加更多图形元素
- 实现手势冲突解决策略
- 添加触摸反馈动画
- 支持更多手势(如长按、快速滑动等)