重要ポイント
Pinch-to-Zoom(ピンチ)は、表示されたコンテンツの拡大率を変更するために2本の指を使用するマルチタッチジェスチャーです。指を近づけると(ピンチクローズ)拡大率が減少し、指を広げると(ピンチオープン)増加します。このジェスチャーは継続的で、システムは指の間の距離をリアルタイムで追跡し、中間イベントを生成して、アプリケーションがスムーズに拡大率を更新できるようにします。
ジェスチャーの仕組みは、2つのタッチポイント間の距離の計算に基づいています。iOSとAndroidはユークリッド距離を使用します:sqrt((x2−x1)² + (y2−y1)²)。現在のスケールは、現在の距離と初期距離の比率として計算されます。初期距離が100pxで150pxになった場合、スケール = 1.5(50%増加)です。iOSは前の状態に対するスケール(デルタ)を渡し、AndroidはScaleGestureDetectorを通じて累積スケールを渡します。
Google Material Design Guidelinesによると、Pinch-to-Zoomは画像、地図、ドキュメントを表示するアプリケーションで必須です。IT Sectrでは、ギャラリーから画像エディターまで、ビジュアルコンテンツを扱うすべてのプロジェクトで、UIPinchGestureRecognizerとScaleGestureDetectorを使用してこのジェスチャーを実装しています。
UIPinchGestureRecognizerは、ピンチジェスチャーを認識するためのUIGestureRecognizerの継続的なサブクラスです。離散的な認識器(UITapGestureRecognizer)とは異なり、.began(指が画面に触れる)、.changed(指の各動作)、.ended(指が離される)の一連のイベントを生成します。
主要なプロパティ:scale(CGFloat)— ジェスチャー開始以降の指間距離の相対的な変化、velocity(CGFloat)— 1秒あたりのスケール変化率。初期スケール = 1.0。指を広げるとスケールが増加し(1.0 → 1.5 → 2.0)、近づけると減少します(1.0 → 0.8 → 0.5)。velocityプロパティは慣性に使用され、.ended後に減衰を伴うスケーリングの継続をアニメーション化できます。
UIPinchGestureRecognizerは、他の認識器(地図上のpan + pinchなど)との同時認識のためのdelegateプロパティをサポートしています。gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)メソッドはtrueを返し、両方のジェスチャーが並行して機能します。Appleは、個々の要素ではなくコンテナビューにUIPinchGestureRecognizerを追加することを推奨しています。これにより、スケーリングのピボットポイントの計算が簡素化されます。
ScaleGestureDetectorは、マルチタッチスケーリングを認識するために設計されたAndroid Support Library(android.view)のクラスです。GestureDetector.onFlingとは異なり、ScaleGestureDetectorは特に2本指ズーム用に構築されており、focusX、focusY(指の間の中心点)とscaleFactor(前のイベントからのスケールデルタ)を自動的に計算します。
ScaleGestureDetectorは、3つのメソッドを持つOnScaleGestureListenerインターフェースを通じて機能します:onScaleBegin(detector)(.beganに相当)、onScale(detector)(.changedに相当、booleanを返す — スケールが受け入れられた場合は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を実装する際、最小および最大スケールを制限することが非常に重要です — これがないと、ユーザーは無限にズームインしたり、見えなくなるまでズームアウトしたりする可能性があります。標準値:最小 = 0.5(元のサイズの50%)、最大 = 3.0(300%)。iOSでは、クランプを使用して制限します:newScale = min(max(currentScale * gesture.scale, minScale), maxScale)。Androidでは、postScale後のチェックを通じて制限します。
ピボットポイントは、指の間の中心点であり、これを基準にスケーリングが実行されます。iOSは2つのタッチの重心を自動的に計算し、location(in:)を通じて渡します — 開発者が手動でピボットを計算する必要はありません。AndroidはScaleGestureDetectorでfocusX/focusYを提供しますが、Matrixを介して適用する場合、ピボットはpostScale(scaleFactor, scaleFactor, focusX, focusY)の第3引数と第4引数として渡されます。ピボットポイントがないと、スケーリングは左上隅(0,0)を基準に実行され、ズーム中にコンテンツが移動します。
IT Sectrの経験によると、正しいピボットポイントの計算は、Pinch-to-Zoomを実装する際のバグの最も一般的な原因です。90%のケースでは、GestureRecognizerとScaleGestureDetectorの組み込み座標を使用するだけで十分であり、手動でピボットを計算する必要はありません。スケーリング中にコンテンツが移動する場合は、focusX/focusYをmatrix.postScale()に渡しているか確認してください。
ジェスチャー終了後のスムーズなUXのために、慣性を追加できます:iOSでは、UIView.animateとスプリングパラメーターを備えたUIPinchGestureRecognizerのvelocityを使用します。Androidでは、OverScrollerまたはValueAnimatorを使用します。慣性は最小/最大スケール境界を超えてはならず、制限を超えた場合はスプリングリターンアニメーションで最終値が制限されます。
0.5から3.0のスケール制限付きでUIImageViewにUIPinchGestureRecognizerを追加します。累積スケールを保持した最小限の実装を示します。
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を介してスケールが適用されます。.endedでは、スケールが1.0にリセットされ、累積値がcurrentScaleに保存されます。これにより、繰り返しのジェスチャー中のスケールドリフトが防止され、新しい各ジェスチャーは現在の状態から開始されます。画像の場合は、CGAffineTransformではなく、minimumZoomScale/maximumZoomScaleを備えたUIScrollViewが推奨されます。
Matrixを使用したScaleGestureDetectorを介して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で累積されます。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分の1に削減されます。
よくある質問
タッチスクリーンでは、ピンチは指の間の距離を追跡しながら2本の指を使用します。MacBookのトラックパッドでは、ズームは同じ状態(.began → .changed → .ended)のUIPinchGestureRecognizerを通じて実装されていますが、タッチソースは静電容量式スクリーンではなくForce Touchトラックパッドです。APIは同じで、仕組みは同一です。
iOS:UIPinchGestureRecognizerからスケールを取得した後、currentScaleを掛けてclamp(min: 0.5, max: 3.0)を適用します。Android:matrix.postScale()の前にcoerceIn(minScale, maxScale)を使用します。UIScrollViewでは、minimumZoomScaleとmaximumZoomScaleプロパティを通じて制限します。制限がないと、スケールが無限大またはゼロになる可能性があります。
いいえ、ピンチには最低2本の指が必要です。1本指ズーム(ダブルタップ + 上/下にドラッグ)には、UIPanGestureRecognizerまたはGestureDetector.onScrollによるカスタム実装が必要です。これは異なるロジックを持つ異なるジェスチャーであり、通常はカメラや地図でのズームに使用されます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。