Android Kotlin 如何实现自定义View类谷歌地图双指缩放功能
自定义View实现谷歌地图式双指缩放平移方案
核心实现思路
- 不推荐使用多子View嵌套方案:房间数量较多时,addView加布局计算的性能开销极大,缩放过程容易卡顿。直接在FloorView的onDraw中通过Canvas矩阵统一变换绘制所有房间,和谷歌地图的渲染逻辑一致,性能更高
- 手势处理直接使用系统原生API:
ScaleGestureDetector处理双指捏合缩放,GestureDetector处理单指拖拽平移,交互完全对齐谷歌地图:缩放中心点始终锚定双指中点,缩放过程中内容无偏移 - 增加缩放范围、平移边界限制,避免缩放过小/过大、拖拽时楼层超出可视区域出现黑边
- 所有房间、楼层的原始坐标保持固定,手势更新时仅修改画布变换矩阵,不需要逐个调整元素位置,渲染开销极低
完整实现代码
1. 房间绘制实体(替代独立RoomView,降低性能开销)
如果需要保留原有RoomView类结构,只需要把绘制逻辑抽成公开draw方法即可,不需要继承View减少布局层级:
import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.RectF data class Room( val rectF: RectF, // 房间原始坐标,固定存储不需要修改 val fillColor: Int = Color.CYAN ) { private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL_AND_STROKE } fun draw(canvas: Canvas) { paint.color = fillColor canvas.drawRect(rectF, paint) } }
2. 支持缩放平移的FloorView实现
import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Matrix import android.graphics.Paint import android.graphics.RectF import android.util.AttributeSet import android.view.GestureDetector import android.view.MotionEvent import android.view.ScaleGestureDetector import android.view.View import kotlin.math.max import kotlin.math.min class FloorView(context: Context, attrs: AttributeSet) : View(context, attrs) { // 楼层底图画笔 private val floorPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL_AND_STROKE color = Color.RED } // 画布变换矩阵 private val drawMatrix = Matrix() // 缩放范围限制,可根据业务调整 private val minScale = 0.2f private val maxScale = 5f // 当前缩放比例、平移偏移量 private var currentScale = 1f private var transX = 0f private var transY = 0f // 手势检测器 private val scaleGestureDetector: ScaleGestureDetector private val gestureDetector: GestureDetector // 楼层原始尺寸 private val floorWidth: Float private val floorHeight: Float private val floorRect: RectF // 房间数据集合 private val roomList = mutableListOf<Room>() init { val screenHeight = context.resources.displayMetrics.heightPixels.toFloat() val screenWidth = context.resources.displayMetrics.widthPixels.toFloat() floorWidth = screenWidth floorHeight = screenHeight floorRect = RectF(0f, 0f, floorWidth, floorHeight) // 双指缩放监听 scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() { override fun onScale(detector: ScaleGestureDetector): Boolean { val scaleFactor = detector.scaleFactor val preScale = currentScale currentScale *= scaleFactor // 限制缩放边界 currentScale = max(minScale, min(currentScale, maxScale)) val realScaleFactor = currentScale / preScale // 锚定双指中心点缩放,和谷歌地图交互一致 val focusX = detector.focusX val focusY = detector.focusY transX = focusX - (focusX - transX) * realScaleFactor transY = focusY - (focusY - transY) * realScaleFactor // 平移边界校准 checkBorder() invalidate() return true } }) // 单指拖拽监听 gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { transX -= distanceX transY -= distanceY checkBorder() invalidate() return true } }) } // 公开添加房间方法 fun addRoom(room: Room) { roomList.add(room) invalidate() } // 边界校验:避免楼层拖出可视范围出现黑边 private fun checkBorder() { val visibleWidth = width.toFloat() val visibleHeight = height.toFloat() val scaledFloorWidth = floorWidth * currentScale val scaledFloorHeight = floorHeight * currentScale // 左右边界限制 transX = if (scaledFloorWidth <= visibleWidth) { (visibleWidth - scaledFloorWidth) / 2f } else { max(visibleWidth - scaledFloorWidth, min(transX, 0f)) } // 上下边界限制 transY = if (scaledFloorHeight <= visibleHeight) { (visibleHeight - scaledFloorHeight) / 2f } else { max(visibleHeight - scaledFloorHeight, min(transY, 0f)) } } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) // 首次加载时居中显示楼层 transX = (w - floorWidth) / 2f transY = (h - floorHeight) / 2f checkBorder() invalidate() } override fun onTouchEvent(event: MotionEvent): Boolean { scaleGestureDetector.onTouchEvent(event) gestureDetector.onTouchEvent(event) return true } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) canvas.save() // 应用变换矩阵 drawMatrix.reset() drawMatrix.postScale(currentScale, currentScale) drawMatrix.postTranslate(transX, transY) canvas.matrix = drawMatrix // 绘制楼层底图 canvas.drawRect(floorRect, floorPaint) // 绘制所有房间,矩阵自动完成坐标变换 roomList.forEach { it.draw(canvas) } canvas.restore() } }
使用方式
在页面中直接给FloorView传入房间数据即可:
val floorView = findViewById<FloorView>(R.id.floorView) // 传入实际业务的房间坐标 floorView.addRoom(Room(RectF(200f, 200f, 600f, 600f))) floorView.addRoom(Room(RectF(800f, 300f, 1200f, 700f), Color.GREEN))
扩展说明
- 如果必须保留RoomView作为子View的结构,只需要在手势更新时遍历所有子View,修改每个子View的translationX、translationY、scaleX、scaleY即可,但房间数量超过50个时会出现明显卡顿,不推荐使用
- 房间点击交互只需要在onTouchEvent中记录点击坐标,通过矩阵逆运算把屏幕坐标转换为楼层原始坐标,再和房间RectF做包含判断即可,不需要给每个房间单独设置点击监听
- 双击缩放、惯性滑动等扩展能力只需要给GestureDetector增加对应回调即可,和现有缩放平移逻辑完全兼容
内容的提问来源于stack exchange,提问作者meniman98
相关产品推荐
相关产品推荐

