GestureDetector + Matrix 实现图片拖动、缩放与旋转等功能
本文介绍了通过GestureDetector、ScaleGestureDetector和自定义RotationGestureDetector实现多功能图片查看组件GestureImageView的方法。主要功能包括:双指缩放(控制缩放系数和范围)、拖动移动(监听滑动事件)、双击缩放(切换大小)和双指旋转(计算角度差)。核心通过Matrix的postScale、postTranslate和postR
文章目录
一个支持拖动、双指缩放、旋转等功能的图片查看器是常见的需求,本文将通过
GestureDetector、 ScaleGestureDetector 以及自定义的 RotationGestureDetector 配合 Matrix 实现一个简易的图片手势处理组件 —— GestureImageView。
一、功能概览
GestureDetector.OnGestureListener、OnDoubleTapListener 是系统提供的手势识别的两个接口,其内部依赖 MotionEvent,通过不同的手势判断逻辑触发回调。
GestureDetector.SimpleOnGestureListener 实现了上述两个接口,可以重写自己想要的方法,比如现在想触发双击时的手势:
private val mGestureDetectorListener = object : GestureDetector.SimpleOnGestureListener() {
//快速点击两次,触发该方法
override fun onDoubleTap(e: MotionEvent?): Boolean {
zoomImage() //在这里实现缩放图片
return true
}
}
private var gestureDetector: GestureDetector = GestureDetector(context, mGestureDetectorListener)
override fun onTouchEvent(event: MotionEvent?): Boolean {
gestureDetector.onTouchEvent(event)
return true
}
上述代码就完成了一个双击手势的监听。除此之外,GestureDetector.SimpleOnGestureListener 还提供了多个其他手势的重写方法:
| 方法名 | 说明 |
|---|---|
onDown(MotionEvent e) |
手指按下时触发 |
onShowPress(MotionEvent e) |
手指按下后短时间未移动或未抬起 |
onSingleTapUp(MotionEvent e) |
抬起时触发的单击事件 |
onScroll(e1, e2, distanceX, distanceY) |
手指滑动时触发 |
onLongPress(MotionEvent e) |
长按触发,如:按住屏幕超过一定时间(默认 500ms)且未滑动,会触发此方法 |
onFling(e1, e2, velocityX, velocityY) |
快速滑动后抬起手指(惯性滑动)时触发 |
onDoubleTap(MotionEvent e) |
双击触发 |
onDoubleTapEvent(MotionEvent e) |
双击过程中的事件触发 |
onSingleTapConfirmed(MotionEvent e) |
确认是单击而非双击的一次点击 |
二、效果图

- 双指缩放:通过
ScaleGestureDetector控制Matrix.postScale()实现 - 拖动移动:通过
GestureDetector.onScroll()控制Matrix.postTranslate()实现 - 双击缩放:通过
GestureDetector.onDoubleTap()实现快速放大或还原 - 双指旋转:通过自定义
RotationGestureDetector控制Matrix.postRotate()实现
三、核心代码结构
1. GestureImageView 继承自 AppCompatImageView
class GestureImageView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : AppCompatImageView(context, attrs)
初始化中设置 scaleType = ScaleType.MATRIX,并注册多个手势监听器:
scaleGestureDetector = ScaleGestureDetector(context, mScaleGestureListener)
gestureDetector = GestureDetector(context, mGestureDetectorListener)
rotationGestureDetector = RotationGestureDetector(mRotationGestureListener)
2. 缩放手势监听 ScaleGestureDetector
onScale()中通过detector.scaleFactor获取缩放系数- 调用
matrix.postScale()实现围绕焦点缩放 - 使用
coerceIn()限定缩放区间避免图片过小或过大
override fun onScale(detector: ScaleGestureDetector?): Boolean {
val preScale = currentScale
currentScale = (currentScale * detector.scaleFactor).coerceIn(0.5f, 3.0f)
val realFactor = currentScale / preScale
matrix.postScale(realFactor, realFactor, detector.focusX, detector.focusY)
imageMatrix = matrix
return true
}
3. 拖动与双击缩放 GestureDetector
- 拖动通过
onScroll()监听并调用matrix.postTranslate()实现 - 双击通过
onDoubleTap()切换缩放倍数并居中
override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
matrix.postTranslate(-distanceX, -distanceY)
imageMatrix = matrix
return true
}
override fun onDoubleTap(e: MotionEvent?): Boolean {
zoomImage()
return true
}
private fun zoomImage() {
if (currentScale == mDefaultScale) {
scaleFactor = if (isZoomed) 0.5f else 2f
matrix.postScale(scaleFactor, scaleFactor, width / 2f, height / 2f)
isZoomed = !isZoomed
imageMatrix = matrix
invalidate()
} else {
setImgToCenter(this)
currentScale = mDefaultScale
isZoomed = false
}
}
4. 自定义 RotationGestureDetector
通过监听两指的角度差,计算旋转角度并传递给 GestureImageView 控制旋转:
class RotationGestureDetector(private val listener: OnRotationGestureListener) {
private var prevAngle = 0f
fun onTouchEvent(event: MotionEvent?) {
if (event?.pointerCount == 2) {
val angle = calculateAngle(event)
val delta = angle - prevAngle
listener.onRotation(this, delta)
prevAngle = angle
}
}
private fun calculateAngle(event: MotionEvent): Float {
val dx = event.getX(1) - event.getX(0)
val dy = event.getY(1) - event.getY(0)
return Math.toDegrees(atan2(dy.toDouble(), dx.toDouble())).toFloat()
}
}
5、居中缩放图片 setImgToCenter()
初始设置图片缩放并居中展示:
val widthScale = img.width.toFloat() / it.intrinsicWidth
val heightScale = img.height.toFloat() / it.intrinsicHeight
val scale = min(widthScale, heightScale)
val dx = (img.width - it.intrinsicWidth * scale) / 2
val dy = (img.height - it.intrinsicHeight * scale) / 2
matrix.postScale(scale, scale)
matrix.postTranslate(dx, dy)
配合 Matrix 灵活组合平移、缩放、旋转变换,效果顺滑自然。可作为自定义图片查看器的基础组件。
GestureImageView完整代码如下:
class GestureImageView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : AppCompatImageView(context, attrs) {
private var isZoomed = false
private var scaleFactor = 2f // 放大倍数
private var mDefaultScale = 1f
/**
* 双指缩放时调用,方法调用顺序:
* 1. onScaleBegin(detector) //1、缩放开始时调用
* 2. onScale(detector) //2、每一帧的缩放变动会调用多次
* 3. ...
* 4. ...
* 5. onScale(...)
* 6. onScaleEnd(detector) //3、缩放结束(手指离开)时调用
*/
private val mScaleGestureListener = object : ScaleGestureDetector.OnScaleGestureListener {
/**
* 当两个手指在屏幕上移动时持续调用。在这个方法中可以通过detector.getScaleFactor()获取缩放因子,更新视图的缩放逻辑
*/
override fun onScale(detector: ScaleGestureDetector?): Boolean {
log("onScale():$currentScale")
if (detector == null) return false
val preScale = currentScale
//detector.scaleFactor 表示当前帧的缩放因子(如1.05表示放大5%,0.95表示缩小5%)
currentScale = (currentScale * detector.scaleFactor).coerceIn(0.5f, 3.0f) //限制缩放比例
val realFactor = currentScale / preScale //计算限制后的实际缩放因子:新的/旧的,进行缩放矫正
//focusX/focusY表示两个手指的中心点(焦点)坐标
val focusX = detector.focusX
val focusY = detector.focusY
isZoomed = (currentScale > mDefaultScale)
matrix.postScale(realFactor, realFactor, focusX, focusY)
imageMatrix = matrix
return true
}
/**
* 缩放手势开始时调用
*/
override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean {
log("onScaleBegin():$currentScale")
return true
}
/**
* 缩放手势结束时调用
*/
override fun onScaleEnd(detector: ScaleGestureDetector?) {
log("onScaleEnd():$currentScale")
}
}
private val mGestureDetectorListener = object : GestureDetector.SimpleOnGestureListener() {
//-----------------GestureDetector.OnGestureListener start-----------------
override fun onDown(e: MotionEvent?): Boolean {
log("onDown():${e?.actionMasked}")
return true
}
/**
* 手指按下后,短暂静止时触发(未抬起/滑动),用于提供视觉提示
*/
override fun onShowPress(e: MotionEvent?) {
log("onShowPress():${e?.actionMasked}")
}
/**
* 户点击(按下再抬起)时调用。 true:表示消费了这个点击事件
*/
override fun onSingleTapUp(e: MotionEvent?): Boolean {
log("onSingleTapUp():${e?.actionMasked}")
return false
}
/**
* 拖动手势中持续触发,表示手指在滑动
*
* @param e1
* @param e2
* @param distanceX
* @param distanceY
* distanceX 和 distanceY 表示当前事件和前一个事件之间的移动距离(不是总距离)。手指滑动方向:
* distanceX:👉 向右滑动为负数,x减小;👈 向左滑动正数,x增加 (preX - curX)
* distanceY:👇 向下滑动负数,y减小;👆 向上滑动正数,y增加 (preY - curY)
*/
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {
log("onScroll() -> e1:${e1?.actionMasked}, e2:${e2?.actionMasked},distanceX:$distanceX,distanceY:$distanceY")
matrix.postTranslate(-distanceX, -distanceY)
imageMatrix = matrix
return true
}
/**
* 用户按住屏幕超过一定时间(默认 500ms)且未滑动,会触发此方法
*/
override fun onLongPress(e: MotionEvent?) {
log("onLongPress():${e?.actionMasked}")
}
/**
* 快速滑动后抬起手指(惯性滑动)时触发
*
* @param e1
* @param e2
* @param velocityX
* @param velocityY
* @return true表示消费了这个fling手势
*/
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
log("onFling() -> e1:${e1?.actionMasked}, e2:${e2?.actionMasked},velocityX:$velocityX,velocityY:$velocityY")
return false
}
//-----------------GestureDetector.OnGestureListener end-----------------
//-----------------GestureDetector.OnDoubleTapListener start-----------------
/**
* 快速点击两次,触发该方法
*/
override fun onDoubleTap(e: MotionEvent?): Boolean {
log("onDoubleTap():${e?.actionMasked}")
zoomImage()
return true
}
/**
* 在双击过程中,down, move, up 都会回调到这里
*/
override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
log("onDoubleTapEvent():${e?.actionMasked}")
return super.onDoubleTapEvent(e)
}
/**
* 确认是单击(而非双击的一部分)时触发
*/
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
log("onSingleTapConfirmed():${e?.actionMasked}")
return super.onSingleTapConfirmed(e)
}
//-----------------GestureDetector.OnDoubleTapListener end-----------------
//-----------------GestureDetector.OnContextClickListener start-----------------
/**
* 支持鼠标右键或触控板的 context click(上下文点击),适用于 Android TV 或外接鼠标设备
*/
override fun onContextClick(e: MotionEvent?): Boolean {
log("onContextClick():${e?.actionMasked}")
return super.onContextClick(e)
}
//-----------------GestureDetector.OnContextClickListener end-----------------
}
private val mRotationGestureListener = object : RotationGestureDetector.OnRotationGestureListener {
override fun onRotation(rotationDetector: RotationGestureDetector, angle: Float) {
currentRotation += angle
val px = width / 2f
val py = height / 2f
matrix.postRotate(angle, px, py)
imageMatrix = matrix
}
}
private val matrix = Matrix()
private var currentScale = 1f //当前缩放了多少倍
private var currentRotation = 0f
private var scaleGestureDetector: ScaleGestureDetector
private var gestureDetector: GestureDetector
private var rotationGestureDetector: RotationGestureDetector
init {
scaleType = ScaleType.MATRIX
scaleGestureDetector = ScaleGestureDetector(context, mScaleGestureListener)
gestureDetector = GestureDetector(context, mGestureDetectorListener)
rotationGestureDetector = RotationGestureDetector(mRotationGestureListener)
setImgToCenter(this)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
scaleGestureDetector.onTouchEvent(event)
gestureDetector.onTouchEvent(event)
rotationGestureDetector.onTouchEvent(event)
return true
}
private fun zoomImage() {
log("currentScale:$currentScale,isZoomed:$isZoomed")
if (currentScale == mDefaultScale) {
scaleFactor = if (isZoomed) 0.5f else 2f
matrix.postScale(scaleFactor, scaleFactor, width / 2f, height / 2f)
isZoomed = !isZoomed
imageMatrix = matrix
invalidate()
} else {
setImgToCenter(this)
currentScale = mDefaultScale
isZoomed = false
}
}
private fun getCurScale(): Float {
val mArr = FloatArray(9)
imageMatrix.getValues(mArr)
return min(mArr[Matrix.MSCALE_X], mArr[Matrix.MSCALE_Y])
}
private fun setImgToCenter(img: ImageView) {
img.post {
val drawable = img.drawable //图片
drawable?.let {
matrix.reset()
//计算宽高比例
val widthScale = img.width.toFloat() / it.intrinsicWidth
val heightScale = img.height.toFloat() / it.intrinsicHeight
//选择较小的缩放比
val scale = min(widthScale, heightScale)
//缩放后的图片尺寸
val scaledWidth = it.intrinsicWidth * scale
val scaleHeight = it.intrinsicHeight * scale
//计算平移量
val dx = (img.width - scaledWidth) / 2
val dy = (img.height - scaleHeight) / 2
matrix.postScale(scale, scale)
matrix.postTranslate(dx, dy)
img.imageMatrix = matrix
}
}
}
}
注意:本文只是为了学习下GestureDetector、Matrix的用法,如果想实现一个完整图片浏览功能,可以参见一些三方的成熟库,如:https://github.com/Baseflow/PhotoView
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)