在 Android 中实现支持多手势交互的自定义 View(Kotlin 完整指南)

本文将手把手教你创建一个支持拖动、缩放、旋转等多种手势交互的自定义 View,并提供完整的代码实现和优化建议。

一、基础实现

1.1 创建自定义 View 骨架

kotlin 复制代码
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 实现基本手势

拖动处理:
kotlin 复制代码
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
}
缩放处理:
kotlin 复制代码
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 旋转手势检测器

kotlin 复制代码
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 边界约束

kotlin 复制代码
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)
}

三、性能优化实现

惯性滑动

kotlin 复制代码
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
    }
}

四、完整布局示例

xml 复制代码
<com.example.app.InteractiveView
    android:id="@+id/interactiveView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#F0F0F0"/>

五、最佳实践建议

  1. 绘制优化:

    kotlin 复制代码
    override fun onDraw(canvas: Canvas) {
        // 避免在绘制过程中创建新对象
        canvas.drawCircle(
            originalRect.centerX(),
            originalRect.centerY(),
            circleRadius,
            paint // 重用预定义的 Paint 对象
        )
    }
  2. 手势优先级处理:

    kotlin 复制代码
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when {
            scaleDetector.isInProgress -> scaleDetector.onTouchEvent(event)
            rotationDetector.isInProgress -> rotationDetector.onTouchEvent(event)
            else -> gestureDetector.onTouchEvent(event)
        }
        return true
    }
  3. 多指触控处理:

    kotlin 复制代码
    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 将支持:

  • 单指拖动
  • 双指缩放(带锚点)
  • 双指旋转
  • 惯性滑动
  • 双击重置
  • 边界约束
  • 多指触控支持

可以通过组合这些手势实现复杂的交互效果,如图片查看器、可操作图表等。


以下是为您整合所有功能的完整实现代码:

kotlin 复制代码
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)
        }
    }
}

功能实现说明

  1. 单指拖动

    • 通过GestureDetector检测滚动事件
    • onScroll中更新offsetX/Y
    • 添加边界约束防止移出可视区域
  2. 双指缩放(带锚点)

    • 使用ScaleGestureDetector检测缩放手势
    • 记录缩放锚点(pivotX, pivotY)
    • 动态调整偏移量保持锚点位置稳定
    • 限制缩放范围(0.5-5倍)
  3. 双指旋转

    • 自定义RotationGestureDetector计算旋转角度
    • 通过两点坐标计算旋转角度差值
    • 更新rotationAngle并限制在0-360度之间
  4. 惯性滑动

    • 使用OverScroller实现流畅的惯性滑动
    • onFling中初始化滑动参数
    • computeScroll中持续更新位置
  5. 双击重置

    • onDoubleTap中重置所有变换参数
    • 重置位置、缩放、旋转到初始状态
  6. 边界约束

    • applyBoundaryConstraints方法计算最大偏移量
    • 根据当前缩放比例动态调整边界限制
    • 在每次位置变化后调用约束方法
  7. 多指触控支持

    • 处理ACTION_POINTER_DOWN/UP事件
    • 跟踪活动指针ID
    • 正确处理多指手势的切换

使用方式

  1. 在XML布局中添加:
xml 复制代码
<com.your.package.AdvancedInteractiveView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#F0F0F0"/>
  1. 自定义属性建议(可选):
xml 复制代码
<resources>
    <declare-styleable name="AdvancedInteractiveView">
        <attr name="minScale" format="float" />
        <attr name="maxScale" format="float" />
        <attr name="shapeColor" format="color" />
    </declare-styleable>
</resources>

性能优化建议

  1. 硬件加速
xml 复制代码
<application android:hardwareAccelerated="true">
  1. 绘制优化
  • 避免在onDraw中创建新对象
  • 使用canvas.saveLayer()替代多次绘制
  • 对于复杂图形使用Bitmap缓存
  1. 手势优化
  • 设置合适的手势检测阈值
  • 使用ViewConfiguration获取系统标准值:
kotlin 复制代码
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
  1. 内存管理
  • 在onDetachedFromWindow中释放资源
  • 使用弱引用持有上下文

可根据需要调整以下参数:

  • circleRadius:初始圆形半径
  • minScale/maxScale:缩放范围限制
  • 颜色和样式通过Paint对象自定义
  • 边界约束计算逻辑调整

实际使用时可扩展以下功能:

  • 添加更多图形元素
  • 实现手势冲突解决策略
  • 添加触摸反馈动画
  • 支持更多手势(如长按、快速滑动等)
相关推荐
robotx1 分钟前
安卓线程相关
android
消失的旧时光-194322 分钟前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon1 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon1 小时前
VSYNC 信号完整流程2
android
dalancon1 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户69371750013842 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android3 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才4 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶4 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle
汪海游龙5 小时前
开源项目 Trending AI 招募 Google Play 内测人员(12 名)
android·github