在 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对象自定义
  • 边界约束计算逻辑调整

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

  • 添加更多图形元素
  • 实现手势冲突解决策略
  • 添加触摸反馈动画
  • 支持更多手势(如长按、快速滑动等)
相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work1 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work1 天前
ch21 签名、校验与发版
kotlin
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui