Pinch-to-Zoom — モバイルアプリケーションにおけるピンチジェスチャーの理解

著者: IT Sectr 公開日: 2026-02-27 読了時間: 8 分
Pinch-to-Zoom(指でつまむ動作)は、ユーザーが画面で2本の指を近づけたり広げたりしてコンテンツを縮小または拡大するマルチタッチズームジェスチャーです。このジェスチャーは、モバイルデバイスで画像、地図、Webページ、ドキュメントを表示するための事実上の標準となっています。Apple HIGとGoogle Material Designによると、Pinch-to-Zoomは、ユーザーがビジュアルコンテンツを扱うすべてのアプリケーションで期待する基本的なジェスチャーです。

重要ポイント

  • Pinch-to-Zoomは、モバイルデバイスでコンテンツを拡大縮小するための2本指のマルチタッチジェスチャーです。
  • iOSでは、.began → .changed → .endedの継続的な追跡を備えたUIPinchGestureRecognizerを通じて処理が行われます。
  • Androidでは、ScaleGestureDetector(Support Library)とonScaleコールバックを使用してMatrix経由でズームを更新します。
  • UIPinchGestureRecognizerには、scale(前の状態に対する比率)とvelocity(速度)のプロパティがあります。
  • ズームを制限するには、指定された最小値/最大値(通常0.5–3.0)の間でスケール係数をクランプします。

Pinch-to-Zoomとは?

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を使用してこのジェスチャーを実装しています。

iOSでのPinch-to-Zoom:UIPinchGestureRecognizer

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を追加することを推奨しています。これにより、スケーリングのピボットポイントの計算が簡素化されます。

AndroidでのPinch-to-Zoom:ScaleGestureDetector

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を使用します。慣性は最小/最大スケール境界を超えてはならず、制限を超えた場合はスプリングリターンアニメーションで最終値が制限されます。

SwiftとKotlinのコード例

例1:iOSでのPinch-to-Zoom(Swift)

0.5から3.0のスケール制限付きでUIImageViewにUIPinchGestureRecognizerを追加します。累積スケールを保持した最小限の実装を示します。

swift
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が推奨されます。

例2:AndroidでのPinch-to-Zoom(Kotlin)

Matrixを使用したScaleGestureDetectorを介してImageViewのスケーリングを実装します。正しいピボットポイントの処理とスケール制限を示します。

kotlin
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に渡されます。

例3:ズーム対応のUIScrollView(iOS)

UIScrollViewは、デリゲートを通じてPinch-to-Zoomを内蔵しています。これは画像とドキュメントに推奨されるアプローチです。ScrollViewがスケール、慣性、スクロールを自動的に管理します。

swift
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プロパティを通じて制限します。制限がないと、スケールが無限大またはゼロになる可能性があります。

Pinch-to-Zoomは1本指で機能しますか?

いいえ、ピンチには最低2本の指が必要です。1本指ズーム(ダブルタップ + 上/下にドラッグ)には、UIPanGestureRecognizerまたはGestureDetector.onScrollによるカスタム実装が必要です。これは異なるロジックを持つ異なるジェスチャーであり、通常はカメラや地図でのズームに使用されます。

まとめ

  • Pinch-to-Zoomは、コンテンツを拡大縮小するための2本指のマルチタッチジェスチャーで、画像、地図、ドキュメントの標準です。
  • iOSのUIPinchGestureRecognizerは、scale(デルタ)とvelocity(速度)を備えた継続的な認識器です。
  • AndroidのScaleGestureDetectorは、ピボットポイント用のonScale()、focusX/focusYを備えた専用検出器です。
  • UIScrollViewの場合、ズームはminimumZoomScale/maximumZoomScaleを通じて組み込まれており、手動実装よりも簡単です。
  • クランプ(0.5–3.0)を使用して常にスケールを制限してください。そうしないと、コンテンツが見えなくなる可能性があります。
  • スケーリングのピボットポイントは指の間の中心です。これがないと、ズーム中にコンテンツが移動します。
  • IT Sectrでは、画像にはUIScrollView、エディターにはカスタムScaleGestureDetectorを使用しています。選択は変換の複雑さに依存します。

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください