Pinch-to-Zoom — hiểu về cử chỉ chụm trong ứng dụng di động

Tác giả: IT Sectr Đã đăng: 2026-02-27 Thời gian đọc: 8 phút
Pinch-to-Zoom (chụm ngón tay) là cử chỉ thu phóng đa chạm trong đó người dùng chụm hoặc xoà hai ngón tay trên màn hình để thu nhỏ hoặc phóng to nội dung. Cử chỉ này đã trở thành tiêu chuẩn thực tế để xem hình ảnh, bản đồ, trang web và tài liệu trên thiết bị di động. Theo Apple HIG và Google Material Design, Pinch-to-Zoom là cử chỉ cơ bản mà người dùng mong đợi trong tất cả các ứng dụng làm việc với nội dung trực quan.

Những điểm chính

  • Pinch-to-Zoom là cử chỉ đa chạm hai ngón tay để thay đổi tỷ lệ nội dung trên thiết bị di động.
  • Trong iOS, việc xử lý được thực hiện thông qua UIPinchGestureRecognizer với theo dõi liên tục .began → .changed → .ended.
  • Android sử dụng ScaleGestureDetector (từ Support Library) với callback onScale để cập nhật thu phóng qua Matrix.
  • UIPinchGestureRecognizer có thuộc tính scale (tỷ lệ so với trạng thái trước) và velocity (tốc độ).
  • Để giới hạn thu phóng, áp dụng clamp của hệ số tỷ lệ giữa các giá trị min/max đã đặt (thường là 0.5–3.0).

Pinch-to-Zoom là gì?

Pinch-to-Zoom (chụm) là cử chỉ đa chạm sử dụng hai ngón tay để thay đổi tỷ lệ của nội dung được hiển thị. Khi chụm ngón tay vào (pinch close), tỷ lệ giảm; khi xoà ra (pinch open), tỷ lệ tăng. Cử chỉ là liên tục: hệ thống theo dõi khoảng cách giữa các ngón tay trong thời gian thực và tạo các sự kiện trung gian, cho phép ứng dụng cập nhật tỷ lệ một cách mượt mà.

Cơ chế của cử chỉ dựa trên việc tính khoảng cách giữa hai điểm chạm. iOS và Android sử dụng khoảng cách Euclid: sqrt((x2−x1)² + (y2−y1)²). Tỷ lệ hiện tại được tính là tỷ lệ giữa khoảng cách hiện tại và khoảng cách ban đầu. Nếu khoảng cách ban đầu là 100px và trở thành 150px — tỷ lệ = 1.5 (tăng 50%). iOS truyền tỷ lệ tương đối so với trạng thái trước (delta), Android truyền tỷ lệ tích lũy qua ScaleGestureDetector.

Theo Google Material Design Guidelines, Pinch-to-Zoom là bắt buộc đối với các ứng dụng hiển thị hình ảnh, bản đồ hoặc tài liệu. Tại IT Sectr, chúng tôi triển khai cử chỉ này với UIPinchGestureRecognizer và ScaleGestureDetector trong tất cả các dự án làm việc với nội dung trực quan — từ thư viện ảnh đến trình chỉnh sửa hình ảnh.

Pinch-to-Zoom trong iOS: UIPinchGestureRecognizer

UIPinchGestureRecognizer là lớp con liên tục của UIGestureRecognizer để nhận dạng cử chỉ chụm. Không giống các bộ nhận dạng rời rạc (UITapGestureRecognizer), nó tạo ra một chuỗi các sự kiện: .began (ngón tay chạm vào màn hình), .changed (mỗi chuyển động của ngón tay), .ended (ngón tay được nhấc lên).

Các thuộc tính chính: scale (CGFloat) — thay đổi tương đối về khoảng cách giữa các ngón tay kể từ khi bắt đầu cử chỉ; velocity (CGFloat) — tốc độ thay đổi tỷ lệ trên giây. Tỷ lệ ban đầu = 1.0. Khi xoà ngón tay, tỷ lệ tăng (1.0 → 1.5 → 2.0); khi chụm vào, giảm (1.0 → 0.8 → 0.5). Thuộc tính velocity được sử dụng cho quán tính: sau .ended, bạn có thể tạo hoạt ảnh tiếp tục thay đổi tỷ lệ với giảm tốc.

UIPinchGestureRecognizer hỗ trợ thuộc tính delegate để nhận dạng đồng thời với các bộ nhận dạng khác (ví dụ, pan + pinch trên bản đồ). Phương thức gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) trả về true và cả hai cử chỉ hoạt động song song. Apple khuyến nghị thêm UIPinchGestureRecognizer vào view chứa thay vì các phần tử riêng lẻ — điều này đơn giản hóa việc tính toán điểm trụ của việc thay đổi tỷ lệ.

Pinch-to-Zoom trong Android: ScaleGestureDetector

ScaleGestureDetector là một lớp từ Android Support Library (android.view) được thiết kế để nhận dạng thay đổi tỷ lệ đa chạm. Không giống GestureDetector.onFling, ScaleGestureDetector được xây dựng đặc biệt cho thu phóng hai ngón tay và tự động tính toán focusX, focusY (điểm trung tâm giữa các ngón tay) và scaleFactor (delta tỷ lệ từ sự kiện trước).

ScaleGestureDetector hoạt động thông qua giao diện OnScaleGestureListener với ba phương thức: onScaleBegin(detector) (tương tự .began), onScale(detector) (tương tự .changed, trả về boolean — true nếu tỷ lệ được chấp nhận), onScaleEnd(detector) (tương tự .ended). Các thuộc tính: scaleFactor — hệ số nhân tỷ lệ so với sự kiện trước (~1.01 khi xoà chậm), focusX/focusY — tọa độ của điểm trung tâm giữa các ngón tay.

Để áp dụng thay đổi tỷ lệ cho một View, thường sử dụng Matrix: view.imageMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY). Matrix hỗ trợ tích lũy các phép biến đổi — mỗi lần gọi postScale thêm một phép biến đổi mới vào phép biến đổi hiện có. Điều quan trọng cần nhớ: ScaleGestureDetector không giới hạn tỷ lệ tối thiểu/tối đa — đó là trách nhiệm của nhà phát triển. Tại IT Sectr, chúng tôi sử dụng ScaleGestureDetector với các giới hạn tỷ lệ tùy chỉnh (0.5–3.0) và hoạt ảnh quay lại khi vượt quá.

Giới hạn thu phóng và điểm trụ

Khi triển khai Pinch-to-Zoom, việc giới hạn tỷ lệ tối thiểu và tối đa là cực kỳ quan trọng — nếu không, người dùng có thể phóng to đến vô cực hoặc thu nhỏ đến mức không nhìn thấy. Giá trị tiêu chuẩn: min = 0.5 (50% kích thước gốc), max = 3.0 (300%). Trên iOS, việc giới hạn được thực hiện thông qua clamp: newScale = min(max(currentScale * gesture.scale, minScale), maxScale). Trên Android — thông qua kiểm tra sau postScale.

Điểm trụ là điểm trung tâm giữa các ngón tay, mà việc thay đổi tỷ lệ được thực hiện tương đối so với nó. iOS tự động tính toán trọng tâm của hai lần chạm và truyền nó qua location(in:) — nhà phát triển không cần tính điểm trụ thủ công. Android cung cấp focusX/focusY trong ScaleGestureDetector, nhưng khi áp dụng qua Matrix, điểm trụ được truyền dưới dạng đối số thứ ba và thứ tư trong postScale(scaleFactor, scaleFactor, focusX, focusY). Không có điểm trụ, việc thay đổi tỷ lệ sẽ được thực hiện tương đối so với góc trên cùng bên trái (0,0), làm cho nội dung bị dịch chuyển khi thu phóng.

Theo kinh nghiệm của IT Sectr, tính toán điểm trụ chính xác là nguồn lỗi phổ biến nhất khi triển khai Pinch-to-Zoom. Trong 90% trường hợp, chỉ cần sử dụng các tọa độ tích hợp của GestureRecognizer và ScaleGestureDetector là đủ — không cần tính điểm trụ thủ công. Nếu nội dung bị dịch chuyển khi thay đổi tỷ lệ, hãy kiểm tra xem bạn có đang truyền focusX/focusY cho matrix.postScale() không.

Quán tính sau khi thu phóng

Để có trải nghiệm người dùng mượt mà sau khi cử chỉ kết thúc, bạn có thể thêm quán tính: trên iOS — sử dụng velocity từ UIPinchGestureRecognizer với UIView.animate và tham số spring; trên Android — OverScroller hoặc ValueAnimator. Quán tính không được vượt quá giới hạn tỷ lệ min/max — giá trị cuối cùng được cắt với hoạt ảnh quay lại spring khi vượt quá giới hạn.

Ví dụ mã trong Swift và Kotlin

Ví dụ 1: Pinch-to-Zoom trên iOS (Swift)

Thêm UIPinchGestureRecognizer vào UIImageView với giới hạn tỷ lệ từ 0.5 đến 3.0. Hiển thị triển khai tối thiểu với bảo toàn tỷ lệ tích lũy.

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
        }
    }
}

Logic: trong .began/.changed, tỷ lệ được áp dụng qua CGAffineTransform với clamp. Trong .ended, tỷ lệ được đặt lại về 1.0 và giá trị tích lũy được lưu trong currentScale. Điều này ngăn chặn sự trôi tỷ lệ trong các cử chỉ lặp lại — mỗi cử chỉ mới bắt đầu từ trạng thái hiện tại. Đối với hình ảnh, nên sử dụng UIScrollView với minimumZoomScale/maximumZoomScale thay vì CGAffineTransform.

Ví dụ 2: Pinch-to-Zoom trên Android (Kotlin)

Triển khai thay đổi tỷ lệ ImageView qua ScaleGestureDetector với Matrix. Hiển thị xử lý điểm trụ chính xác và giới hạn tỷ lệ.

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 tùy chỉnh ghi đè onTouchEvent, chuyển tiếp các sự kiện đến ScaleGestureDetector. OnScaleListener.onScale() được gọi ở mỗi chuyển động ngón tay. detector.scaleFactor là delta (1.01 khi xoà chậm). currentScale tích lũy với coerceIn để clamp. focusX/focusY — điểm trung tâm giữa các ngón tay, được truyền cho postScale để thay đổi tỷ lệ điểm trụ chính xác.

Ví dụ 3: UIScrollView hỗ trợ thu phóng (iOS)

UIScrollView có hỗ trợ Pinch-to-Zoom tích hợp thông qua delegate. Đây là cách tiếp cận ưa thích cho hình ảnh và tài liệu — ScrollView tự động quản lý tỷ lệ, quán tính và cuộn.

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 tự động tạo UIPinchGestureRecognizer, quản lý việc thay đổi tỷ lệ thông qua biến đổi CALayer và thêm hiệu ứng nảy khi vượt quá giới hạn. Mã được rút gọn chỉ còn đặt min/maxZoomScale và trả về viewForZooming. Tại IT Sectr, chúng tôi sử dụng UIScrollView cho tất cả hình ảnh toàn màn hình — điều này giảm khối lượng mã đi 5 lần so với triển khai thủ công qua UIPinchGestureRecognizer.

Câu hỏi thường gặp

Chụm ngón tay khác gì với chụm trên bàn di chuột?

Trên màn hình cảm ứng, chụm sử dụng hai ngón tay với theo dõi khoảng cách giữa chúng. Trên bàn di chuột MacBook, thu phóng được triển khai thông qua UIPinchGestureRecognizer với cùng các trạng thái (.began → .changed → .ended), nhưng nguồn chạm là bàn di chuột với Force Touch, không phải màn hình điện dung. API giống nhau, cơ chế giống hệt.

Làm thế nào để giới hạn thu phóng tối thiểu và tối đa?

Trên iOS: sau khi nhận tỷ lệ từ UIPinchGestureRecognizer, nhân với currentScale và áp dụng clamp(min: 0.5, max: 3.0). Trên Android: sử dụng coerceIn(minScale, maxScale) trước matrix.postScale(). Trong UIScrollView — thông qua các thuộc tính minimumZoomScale và maximumZoomScale. Nếu không giới hạn, tỷ lệ có thể lên đến vô cực hoặc về không.

Pinch-to-Zoom có hoạt động với một ngón tay không?

Không, chụm yêu cầu ít nhất hai ngón tay. Để thu phóng một ngón tay (nhấp đúp + kéo lên/xuống), cần triển khai tùy chỉnh thông qua UIPanGestureRecognizer hoặc GestureDetector.onScroll. Đây là một cử chỉ khác với logic khác — thường được sử dụng để thu phóng trong máy ảnh hoặc bản đồ.

Tổng kết

  • Pinch-to-Zoom là cử chỉ đa chạm hai ngón tay để thay đổi tỷ lệ nội dung, tiêu chuẩn cho hình ảnh, bản đồ và tài liệu.
  • UIPinchGestureRecognizer trong iOS là bộ nhận dạng liên tục với scale (delta) và velocity (tốc độ).
  • ScaleGestureDetector trong Android là bộ dò chuyên dụng với onScale(), focusX/focusY cho điểm trụ.
  • Đối với UIScrollView, thu phóng được tích hợp qua minimumZoomScale/maximumZoomScale — đơn giản hơn triển khai thủ công.
  • Luôn giới hạn tỷ lệ (0.5–3.0) qua clamp — nếu không, nội dung có thể trở nên vô hình.
  • Điểm trụ thay đổi tỷ lệ là trung tâm giữa các ngón tay; không có nó, nội dung bị dịch chuyển khi thu phóng.
  • Tại IT Sectr, chúng tôi sử dụng UIScrollView cho hình ảnh và ScaleGestureDetector tùy chỉnh cho trình chỉnh sửa — lựa chọn phụ thuộc vào độ phức tạp của các phép biến đổi.

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm