本文将手把手教你创建一个支持拖动、缩放、旋转等多种手势交互的自定义 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
}
// 其他实现将在下文展开...
}
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
}
}
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()
}
}
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)
双指旋转
RotationGestureDetector
计算旋转角度rotationAngle
并限制在0-360度之间惯性滑动
OverScroller
实现流畅的惯性滑动onFling
中初始化滑动参数computeScroll
中持续更新位置双击重置
onDoubleTap
中重置所有变换参数边界约束
applyBoundaryConstraints
方法计算最大偏移量多指触控支持
ACTION_POINTER_DOWN/UP
事件<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">
canvas.saveLayer()
替代多次绘制ViewConfiguration
获取系统标准值:private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
可根据需要调整以下参数:
circleRadius
:初始圆形半径minScale/maxScale
:缩放范围限制实际使用时可扩展以下功能: