要点
Pinch-to-Zoom(捏合或挤压)——一种使用两根手指改变显示内容比例的多点触控手势。当手指并拢(pinch close)时比例缩小,当手指张开(pinch open)时比例增大。该手势是连续的:系统实时跟踪手指之间的距离并生成中间事件,使应用程序能够平滑地更新比例。
该手势的机制基于计算两个触摸点之间的距离。iOS 和 Android 使用欧几里得距离:sqrt((x2-x1)² + (y2-y1)²)。当前 scale 计算为当前距离与初始距离的比率。如果初始距离为 100px,变为 150px — scale = 1.5(增加 50%)。iOS 传递相对于先前状态的 scale(增量),Android 通过 ScaleGestureDetector 传递累积的 scale。
根据 Google Material Design 指南,Pinch-to-Zoom 对于显示图片、地图或文档的应用程序是必要的。在 IT Sectr,我们在所有处理视觉内容的项目中——从图库到图像编辑器——使用 UIPinchGestureRecognizer 和 ScaleGestureDetector 实现此手势。
UIPinchGestureRecognizer——UIGestureRecognizer 的连续子类,用于识别捏合手势。与离散识别器(UITapGestureRecognizer)不同,它生成一系列事件:.began(手指触摸屏幕)、.changed(手指的每次移动)、.ended(手指抬起)。
关键属性:scale(CGFloat)——从手势开始到手指之间距离的相对变化;velocity(CGFloat)——每秒 scale 变化的速度。初始 scale = 1.0。当手指张开时 scale 增加(1.0 → 1.5 → 2.0),当并拢时——减小(1.0 → 0.8 → 0.5)。velocity 属性用于惯性:在 .ended 之后,可以动画化缩放继续并逐渐衰减。
UIPinchGestureRecognizer 支持 delegate 属性以与其他识别器同时工作(例如,地图上的 pan + pinch)。gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)方法返回 true,两个手势并行工作。Apple 建议将 UIPinchGestureRecognizer 添加到容器视图而不是单个元素——这简化了缩放的枢轴点计算。
ScaleGestureDetector——来自 Android Support Library(android.view)的类,用于识别多点触控缩放。与 GestureDetector.onFling 不同,ScaleGestureDetector 专为两指缩放设计,并自动计算 focusX、focusY(手指之间的中心点)和 scaleFactor(相对于先前事件的缩放增量)。
ScaleGestureDetector 通过 OnScaleGestureListener 接口工作,具有三个方法:onScaleBegin(detector)(相当于 .began)、onScale(detector)(相当于 .changed,返回布尔值——如果缩放被接受则为 true)、onScaleEnd(detector)(相当于 .ended)。属性:scaleFactor——相对于先前事件的缩放乘数(慢速张开时约为 1.01),focusX/focusY——手指之间的中心坐标。
要将缩放应用于 View,通常使用 Matrix:view.imageMatrix.postScale(scaleFactor,scaleFactor,focusX,focusY)。Matrix 支持变换的累积——每次调用 postScale 都会向现有变换添加新的变换。重要的是:ScaleGestureDetector 不限制最小/最大缩放比例——这是开发人员的任务。在 IT Sectr,我们使用具有自定义缩放边界(0.5–3.0)和超出时返回动画的 ScaleGestureDetector。
在实现 Pinch-to-Zoom 时,限制最小和最大缩放比例至关重要——否则用户可以将图像无限放大或缩小到不可见。标准值:min = 0.5(原始大小的 50%),max = 3.0(300%)。在 iOS 中,通过 clamp 进行限制:newScale = min(max(currentScale * gesture.scale,minScale),maxScale)。在 Android 中——通过在 postScale 之后检查。
枢轴点——手指之间的中心,缩放相对于该点进行。iOS 自动计算两次触摸的中心点并通过 location(in:)传递——开发人员无需手动计算枢轴。Android 在 ScaleGestureDetector 中提供 focusX/focusY,但在通过 Matrix 应用时,枢轴作为第三个和第四个参数传递给 postScale(scaleFactor,scaleFactor,focusX,focusY)。如果没有枢轴点,缩放将相对于左上角(0,0)进行,这会导致缩放时内容偏移。
根据 IT Sectr 的经验,正确计算枢轴点是实现 Pinch-to-Zoom 时最常见的错误来源。在 90% 的情况下,使用 GestureRecognizer 和 ScaleGestureDetector 的内置坐标就足够了,无需手动计算枢轴。如果内容在缩放时偏移——检查是否将 focusX/focusY 传递给了 matrix.postScale()。
为了手势完成后获得流畅的用户体验,可以添加惯性:在 iOS 中——使用 UIPinchGestureRecognizer 的 velocity 对 UIView.animate 进行 spring 参数动画;在 Android 中——使用 OverScroller 或 ValueAnimator。惯性不应超出 min/max scale 边界——最终值在超出时使用 spring 返回动画进行裁剪。
向 UIImageView 添加 UIPinchGestureRecognizer,缩放限制为 0.5 到 3.0。展示保留累积缩放的最小实现。
import UIKit
class ZoomableImageViewController: UIViewController {
@IBOutlet private var imageView: UIImageView!
private var currentScale: CGFloat = 1.0
private let minScale: CGFloat = 0.5
private let maxScale: CGFloat = 3.0
override func viewDidLoad() {
super.viewDidLoad()
let pinch = UIPinchGestureRecognizer(
target: self,
action: #selector(handlePinch(_:))
)
imageView.addGestureRecognizer(pinch)
imageView.isUserInteractionEnabled = true
}
@objc private func handlePinch(_: UIPinchGestureRecognizer) {
if gestureRecognizer.state == .began
|| gestureRecognizer.state == .changed {
let newScale = currentScale * gestureRecognizer.scale
imageView.transform = CGAffineTransform(
scaleX: min(max(newScale, minScale), maxScale),
y: min(max(newScale, minScale), maxScale)
)
}
if gestureRecognizer.state == .ended {
currentScale = min(max(
currentScale * gestureRecognizer.scale,
minScale
), maxScale)
gestureRecognizer.scale = 1.0
}
}
}
逻辑:在 .began/.changed 中,通过 CGAffineTransform 和 clamp 应用缩放。在 .ended 中,scale 重置为 1.0,累积值保存在 currentScale 中。这可以防止多次手势中缩放比例的漂移——每个新手势从当前状态开始。对于图像,建议使用带有 minimumZoomScale/maximumZoomScale 的 UIScrollView 而不是 CGAffineTransform。
通过 ScaleGestureDetector 和 Matrix 实现 ImageView 的缩放。展示枢轴点的正确处理和缩放限制。
import android.content.Context
import android.graphics.Matrix
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.widget.ImageView
class PinchZoomImageView(context: Context) : ImageView(context) {
private val matrix = Matrix()
private var currentScale = 1f
private val minScale = 0.5f
private val maxScale = 3.0f
private val scaleDetector =
ScaleGestureDetector(context, ScaleListener())
private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
currentScale = (currentScale * detector.scaleFactor)
.coerceIn(minScale, maxScale)
matrix.postScale(
currentScale,
currentScale,
detector.focusX,
detector.focusY
)
imageMatrix = matrix
return true
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
scaleDetector.onTouchEvent(event)
return true
}
}
自定义 ImageView 覆盖 onTouchEvent,将事件传递给 ScaleGestureDetector。OnScaleListener.onScale()在每次手指移动时调用。detector.scaleFactor — 增量(慢速张开时为 1.01)。currentScale 使用 coerceIn 累积以进行 clamp。focusX/focusY — 手指之间的中心点,传递给 postScale 以进行正确的枢轴缩放。
UIScrollView 通过委托内置了对 Pinch-to-Zoom 的支持。这是图像和文档的首选方法——ScrollView 自动管理缩放、惯性和滚动。
class ScrollableImageViewController: UIViewController,
UIScrollViewDelegate {
@IBOutlet private var scrollView: UIScrollView!
@IBOutlet private var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView.minimumZoomScale = 0.5
scrollView.maximumZoomScale = 3.0
scrollView.delegate = self
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
}
UIScrollView 独立创建 UIPinchGestureRecognizer,通过 CALayer 变换管理缩放,并在超出边界时添加弹跳效果。代码简化为设置 min/maxZoomScale 和返回 viewForZooming。在 IT Sectr,我们对所有全屏图像使用 UIScrollView——与通过 UIPinchGestureRecognizer 手动实现相比,这减少了 5 倍的代码量。
常见问题
在触摸屏上,pinch 使用两根手指并跟踪它们之间的距离。在 MacBook 触控板上,缩放通过 UIPinchGestureRecognizer 实现,具有相同的状态(.began → .changed → .ended),但触摸来源是带有 Force Touch 的触控板,而不是电容屏。API 相同,机制相同。
在 iOS 中:从 UIPinchGestureRecognizer 获取 scale 后,乘以 currentScale 并应用 clamp(min:0.5,max:3.0)。在 Android 中:通过 coerceIn(minScale,maxScale)在 matrix.postScale()之前。在 UIScrollView 中——通过 minimumZoomScale 和 maximumZoomScale 属性。如果没有限制,缩放比例可能会变为无限或零。
不可以,pinch 至少需要两根手指。对于单指缩放(双击 + 向上/向下拖动),需要通过 UIPanGestureRecognizer 或 GestureDetector.onScroll 进行自定义实现。这是另一种具有不同逻辑的手势——通常用于相机或地图中的缩放。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。