ประเด็นสำคัญ
Pinch-to-Zoom (การหยิบ) เป็นท่าภาษ์แบบมัลติทัชที่ใช้สองนิ้วเพื่อเปลี่ยนมาตรฐานของเนื้อหาที่แสดงผล เมื่อนำนิ้วเข้าหากัน (pinch close) มาตรฐานจะลดลง เมื่อก้างออก (pinch open) จะเพิ่มขึ้น ท่าภาษ์นี้เป็นแบบต่อเนื่อง: ระบบติดตามระยะห่างระหว่างนิ้วแบบเรียลไทม์และสร้างเหตุการณ์ข้ั้นกลาง ทำให้แอปพลิเคชั่นสามารถอัพเดตมาตรฐานได้อย่างลื่นไหล
กลไกของท่าภาษ์อึงอิงการคำนวณระยะห่างระหว่างสองจุดสัมผัส iOS และ Android ใช้ระยะห่างแบบยุคลิด: sqrt((x2−x1)² + (y2−y1)²) มาตรฐานปัจจุบันคำนวณเป็นอัตราส่วนระหว่างระยะห่างปัจจุบันกับระยะห่างเริ่มต้น หากระยะห่างเริ่มต้นเป็น 100px และกลายเป็น 150px — scale = 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) — อัตราการเปลี่ยนแปลงมาตรฐานต่อวินาที 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 ส่งคืน boolean — true หากยอมรับมาตรฐาน) onScaleEnd(detector) (เสมือน .ended) คุณสมบัติ: scaleFactor — ตัวคูณมาตรฐานที่สัมพันธ์กับเหตุการณ์ก่อนหน้า (~1.01 สำหรับการก้างช้าๆ) focusX/focusY — พิกัดของจุดกึ่งกลางระหว่างนิ้ว
เพื่อใช้การขยายขนาดกับ View โดยปกติจะใช้ Matrix: view.imageMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY) Matrix รองรับการสะสมการแปลงรูป — แต่ละการเรียก postScale จะเพิ่มการแปลงรูปใหม่ใส่การแปลงรูปที่มีอยู่ สิ่งสำคัญที่ต้องจำ: ScaleGestureDetector ไม่จำกัดมาตรฐานต่ำสุด/สูงสุด — นั่นคือหน้าที่ของนักพัฒนา ที่ IT Sectr เราใช้ ScaleGestureDetector กับขอบเขตมาตรฐานที่กำหนดเอง (0.5–3.0) และแอนิเมชั่นการกลับสู่สถานะเดิมเมื่อเกินขอบเขต
เมื่อนำ 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 ให้ focusX/focusY ใน ScaleGestureDetector แต่เมื่อใช้งานผ่าน Matrix จุดหมุนจะถูกส่งเป็นอาร์กิวเมนที่สามและสี่ใน postScale(scaleFactor, scaleFactor, focusX, focusY) หากไม่มีจุดหมุน การขยายขนาดจะดำเนินการสัมพันธ์กับมุมบนซ้าย (0,0) ทำให้เนื้อหาเคลื่อนที่ในระหว่างการซูม
จากประสบการณ์ของ IT Sectr การคำนวณจุดหมุนที่ถูกต้องเป็นแหล่งของข้อผิดพลาดที่พบบ่อยที่สุดเมื่อนำ Pinch-to-Zoom ไปใช้ ใน 90% ของกรณี การใช้พิกัดในตัวของ GestureRecognizer และ ScaleGestureDetector ก็เพียงพอ — ไม่ต้องคำนวณจุดหมุนด้วยตนเอง หากเนื้อหาเคลื่อนที่ในระหว่างการขยายขนาด ให้ตรวจสอบว่าคุณกำลังส่ง focusX/focusY ไปยัง matrix.postScale() หรือไม่
สำหรับ UX ที่ลื่นไหลหลังจากท่าภาษ์สิ้นสุด คุณสามารถเพิ่มแรงเชืองได้: ใน iOS — ใช้ velocity จาก UIPinchGestureRecognizer กับ UIView.animate และพารามิเตอร์ spring ใน Android — OverScroller หรือ ValueAnimator แรงเชืองไม่ควรเกินขอบเขตมาตรฐาน min/max — ค่าสุดท้ายจะถูกตัดด้วยแอนิเมชั่นการกลับคืนแบบ spring เมื่อเกินขอบเขต
เพิ่ม UIPinchGestureRecognizer ไปยัง UIImageView ด้วยการจำกัดมาตรฐานตั้งแต่ 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 ด้วยการคลัมป์ ใน .ended scale จะถูกรีเซ็ตเป็น 1.0 และค่าสะสมจะถูกเก็บไว้ใน currentScale ซึ่งป้องกันการเลื่อนไหลของมาตรฐานในระหว่างท่าภาษ์ซ้ำๆ — แต่ละท่าภาษ์ใหม่จะเริ่มจากสถานะปัจจุบัน สำหรับรูปภาพ แนะนำ UIScrollView ด้วย minimumZoomScale/maximumZoomScale แทน CGAffineTransform
นำไปใช้การขยายขนาด ImageView ผ่าน ScaleGestureDetector กับ Matrix แสดงการจัดการจุดหมุนที่ถูกต้องและการจำกัดมาตรฐาน
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 สำหรับรูปภาพเต็มจอ — ซึ่งช่วยลดปริมาตร์โค้ดได้ถึง 5 เท่าเมื่อเปรียบเทียบกับการดำเนินการด้วยตนเองผ่าน UIPinchGestureRecognizer
คำถามที่พบบ่อย
บนหน้าจอสัมผัส การหยิบ ใช้สองนิ้วโดยการติดตามระยะห่างระหว่างนิ้ว บนแพ็ดทรากเป็ด MacBook การซูมจะถูกนำไปใช้ผ่าน UIPinchGestureRecognizer ด้วยสถานะเดียวกัน (.began → .changed → .ended) แต่แหล่งของการสัมผัสคือแพ็ดทรากเป็ด Force Touch ไม่ใช่หน้าจอแบบเคปาซิเตีฟ API เหมือนกัน กลไกเหมือนกัน
ใน iOS: หลังได้รับ scale จาก UIPinchGestureRecognizer ให้คูณด้วย currentScale และใช้ clamp(min: 0.5, max: 3.0) ใน Android: ใช้ coerceIn(minScale, maxScale) ก่อน matrix.postScale() ใน UIScrollView — ผ่านคุณสมบัติ minimumZoomScale และ maximumZoomScale หากไม่มีการจำกัด มาตรฐานอาจเพิ่มขึ้นไปจนอนันต์หรือลดลงเป็นศูนย์
ไม่ การหยิบต้องการอย่างน้อย สองนิ้ว สำหรับการซูมด้วยนิ้วเดียว (ดับเบิ้ลแบบคู่ + ลากขึ้น/ลง) จำเป็นต้องมีการนำไปใช้แบบกำหนดเองผ่าน UIPanGestureRecognizer หรือ GestureDetector.onScroll นี่เป็นคนละท่าภาษ์ที่แตกต่างกันโดยมีตรรกะที่แตกต่างกัน — โดยปกติใช้สำหรับการซูมในกล้องหรือแผนที่
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ