ประเด็นสำคัญ
การปัด คือการเลื่อนนิ้วอย่างรวดเร็วบนพื้นผิวหน้าจอที่สิ้นสุดด้วยการยกนิ้ว การปัดอาจสั้น (พลิกผ่านองค์ประกอบ) หรือยาว (เปิดม่าน) แต่ลักษณะสำคัญคือความไม่ต่อเนื่อง: ระบบรู้จักมันเป็นเหตุการณ์เดียว ไม่ใช่ลำดับพิกัดต่อเนื่อง
ระบบปฏิบัติการมือถือตรวจจับการปัดตามพารามิเตอร์สามตัว: ระยะทางขั้นต่ำ (เกณฑ์) ความเร็วขั้นต่ำ (velocity) และทิศทาง หากนิ้วเคลื่อนที่ช้าหรือระยะทางไม่เพียงพอ ระบบจะแปลความหมายท่าทางเป็นการเลื่อนหรือการแตะ ค่าเกณฑ์แตกต่างกันระหว่างแพลตฟอร์ม: iOS ใช้ค่าคงที่ใน UISwipeGestureRecognizer พร้อมเกณฑ์คงที่ Android ใช้ ViewConfiguration.getScaledTouchSlop() ซึ่งส่งคืน ~8–16 dp ขึ้นอยู่กับความหนาแน่นของหน้าจอ
ตาม เอกสารสำหรับนักพัฒนา Android onFling (การปัด) ใน GestureDetector จะทำงานที่ความเร็วเกิน 100 px/s และระยะทางตั้งแต่ 50px ที่ IT Sectr เราใช้การปัดเพื่อการนำทาง (แกลเลอรี อินเทอร์เฟซการ์ด การเริ่มต้นใช้งาน) — ท่าทางนี้ใช้งานง่ายสำหรับผู้ใช้ทุกวัย
การปัดและการเลื่อน (pan/drag) เป็นท่าทางคนละประเภท แม้ว่าทั้งสองจะเกี่ยวข้องกับการขยับนิ้วบนหน้าจอ การปัดเป็นแบบไม่ต่อเนื่องและหมายถึง "ดำเนินการหนึ่งอย่าง" ในขณะที่การเลื่อนเป็นแบบต่อเนื่องและหมายถึง "เคลื่อนย้ายวัตถุตราบใดที่นิ้วยังอยู่บนหน้าจอ"
| พารามิเตอร์ | การปัด (Swipe) | การเลื่อน/ลาก (Pan/Drag) |
|---|---|---|
| ประเภท | ไม่ต่อเนื่อง | ต่อเนื่อง |
| เหตุการณ์ | เดี่ยว (รู้จัก/ไม่รู้จัก) | หลายรายการ (.began → .changed → .ended) |
| ความเร็ว | สูง (เคลื่อนที่เร็ว) | ใดก็ได้ (เคลื่อนที่ช้า) |
| ตัวอย่าง iOS | UISwipeGestureRecognizer | UIPanGestureRecognizer |
| ตัวอย่าง Android | GestureDetector.onFling() | GestureDetector.onScroll() |
| สถานการณ์ | ลบอีเมล พลิกหน้า | เคลื่อนย้ายแผนที่ ลากไฟล์ |
| การติดตามพิกัด | ไม่ (เฉพาะทิศทาง) | ใช่ (อัปเดตพิกัดต่อเนื่อง) |
การเลือกระหว่างการปัดและการเลื่อนขึ้นอยู่กับงาน สำหรับการกระทำกับองค์ประกอบ (ลบ เก็บถาวร เปิดเมนู) ใช้การปัด สำหรับการจัดการโดยตรง (เคลื่อนย้าย ปรับขนาด) ใช้การเลื่อน การผสมท่าทางบนองค์ประกอบเดียวกันต้องใช้ require(toFail:) หรือตรรกะแบบกำหนดเอง
UISwipeGestureRecognizer เป็นคลาสย่อยของ UIGestureRecognizer ที่รู้จักการปัดแบบไม่ต่อเนื่องในหนึ่งในสี่ทิศทาง ต่างจาก UIPanGestureRecognizer ตรงที่ไม่สร้างเหตุการณ์ .changed ระหว่างกลาง: เมื่อรู้จักแล้ว การกระทำจะถูกเรียกเพียงครั้งเดียว
คุณสมบัติ direction (UISwipeGestureRecognizer.Direction) กำหนดทิศทางที่ติดตาม — .left, .right, .up, .down สามารถรวมทิศทางผ่านมาสก์: [.left, .right] สำหรับการปัดแนวนอน คุณสมบัติ numberOfTouchesRequired กำหนดจำนวนนิ้ว — ค่าเริ่มต้นคือ 1 UISwipeGestureRecognizer ไม่มีเกณฑ์ระยะทางที่กำหนดค่าได้ — iOS ใช้ค่าในตัวประมาณ 20px ซึ่งเพียงพอสำหรับการรู้จำที่เชื่อถือได้
เนื่องจาก UISwipeGestureRecognizer เป็นตัวรู้จำแบบไม่ต่อเนื่อง มันจึงไม่ขัดแย้งกับ UIPanGestureRecognizer: การเคลื่อนที่ช้าจะเรียก pan การเคลื่อนที่เร็วจะเรียก swipe อย่างไรก็ตาม เมื่ออยู่ร่วมกับ UITapGestureRecognizer ในวิวเดียวกัน อาจต้องใช้ require(toFail:) เพื่อป้องกันการรู้จำการแตะที่ผิดพลาด
ใน Android API หลักสำหรับการรู้จำการปัดคือ GestureDetector พร้อม callback OnGestureListener โดยเฉพาะเมธอด onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) GestureDetector วิเคราะห์ตำแหน่งเริ่มต้นและสุดท้ายของนิ้ว ความเร็วการเคลื่อนที่ตามแกน และระยะทาง จากนั้นเรียก onFling เมื่อเกินเกณฑ์
เกณฑ์ขั้นต่ำกำหนดค่าได้ผ่าน ViewConfiguration: VelocityTracker.getMinVelocity() (~100 px/s) และ ViewConfiguration.getScaledTouchSlop() (~8–16 dp) OnSwipeTouchListener แบบกำหนดเองเป็นรูปแบบที่พบได้ทั่วไปซึ่งทำให้ API ง่ายขึ้น: นักพัฒนาไม่ต้องคำนวณทิศทางและความเร็วด้วยตนเอง Android Support Library ไม่มี "SwipeDetector" ในตัว ดังนั้น wrapper แบบกำหนดเองจึงเป็นวิธีแก้ปัญหามาตรฐาน
สำหรับการปัดในรายการ Google แนะนำให้ใช้ ItemTouchHelper.SimpleCallback จาก RecyclerView ซึ่งสรุปการตรวจจับท่าทาง แอนิเมชัน และ callback onSwiped() ที่ IT Sectr เราใช้ SwipeListener แบบกำหนดเองสำหรับ ViewPager2 และอินเทอร์เฟซการ์ด — ซึ่งให้การควบคุมความไวในการปัดโดยไม่ลดประสิทธิภาพ
สำหรับรายการ (UITableView, RecyclerView) Apple และ Google มี API เฉพาะทาง ที่แทนที่การเพิ่ม UISwipeGestureRecognizer ด้วยตนเอง API เหล่านี้รองรับปุ่มการกระทำ แอนิเมชันแบบกำหนดเอง และท่าทาง "ปัดเพื่อลบ" — รูปแบบมาตรฐานในแอปพลิเคชันมือถือ
ใน iOS (11+) ใช้ UISwipeActionsConfiguration: เมธอด tableView(_:leadingSwipeActionsConfigurationForRowAt:) ส่งคืนอาร์เรย์ของ UIContextualAction — ปุ่มที่มีชื่อเรื่อง รูปแบบ (destructive, normal) และตัวจัดการ ตัวอย่างเช่น "ลบ" — .destructive พื้นหลังสีแดง "เก็บถาวร" — .normal พื้นหลังสีเทา ระบบจะสร้างแอนิเมชันการปรากฏของปุ่มและเรียกบล็อก completion หลังจากแตะ
ใน Android ItemTouchHelper.SimpleCallback แนบกับ RecyclerView ผ่าน ItemTouchHelper.attachToRecyclerView() พารามิเตอร์รวมถึงธงทิศทาง (start, end, up, down) และ callback onSwiped(viewHolder, direction) ภายใน onSwiped จะเรียก adapter.notifyItemRemoved(position) เพื่อลบรายการด้วยแอนิเมชัน ItemTouchHelper จัดการท่าทาง การขัดแย้งกับการเลื่อน และการคืนรายการโดยอัตโนมัติหากการปัดไม่สมบูรณ์ ตาม Google I/O 2024 ItemTouchHelper ถูกใช้ใน 70% ของแอป Android ที่มีการปัดในรายการ
เพิ่ม UISwipeGestureRecognizer ให้กับวิวเพื่อกลับไปยังหน้าจอก่อนหน้า โค้ดแสดงการกำหนดค่าตัวรู้จำขั้นต่ำด้วยทิศทาง .left
import UIKit
class CardViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let swipeLeft = UISwipeGestureRecognizer(
target: self,
action: #selector(handleSwipeLeft(_:))
)
swipeLeft.direction = .left
swipeLeft.numberOfTouchesRequired = 1
view.addGestureRecognizer(swipeLeft)
}
@objc private func handleSwipeLeft(_: UISwipeGestureRecognizer) {
guard let nav = navigationController else { return }
if nav.viewControllers.count > 1 {
nav.popViewController(animated: true)
} else {
nav.dismiss(animated: true)
}
}
}
ตัวรู้จำแนบกับวิวรากของคอนโทรลเลอร์ การตรวจสอบ navigationController.viewControllers.count ทำให้แน่ใจว่า pop จะดำเนินการก็ต่อเมื่อมีหน้าจอก่อนหน้าในสแตก สำหรับคอนโทรลเลอร์แบบ modal ใช้ dismiss โค้ดไม่ต้องการการตรวจสอบสถานะ — UISwipeGestureRecognizer เป็นแบบไม่ต่อเนื่องและเรียกการกระทำเมื่อรู้จำสำเร็จเท่านั้น
ใช้ wrapper GestureDetector อย่างง่ายที่กำหนดทิศทางการปัดและเรียก callback onSwipeLeft/Right
import android.content.Context
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
open class OnSwipeTouchListener(context: Context) : View.OnTouchListener {
private val gestureDetector = GestureDetector(context, GestureListener())
override fun onTouch(v: View, event: MotionEvent): Boolean {
return gestureDetector.onTouchEvent(event)
}
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
companion object {
private const val SWIPE_THRESHOLD = 100
private const val SWIPE_VELOCITY_THRESHOLD = 100
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
val diffX = e2.x - e1!!.x
val diffY = e2.y - e1.y
if (Math.abs(diffX) > Math.abs(diffY)) {
if (Math.abs(diffX) > SWIPE_THRESHOLD
&& Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD
) {
if (diffX > 0) onSwipeRight()
else onSwipeLeft()
return true
}
} else {
if (Math.abs(diffY) > SWIPE_THRESHOLD
&& Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD
) {
if (diffY > 0) onSwipeDown()
else onSwipeUp()
return true
}
}
return false
}
}
open fun onSwipeLeft() {}
open fun onSwipeRight() {}
open fun onSwipeUp() {}
open fun onSwipeDown() {}
}
OnSwipeTouchListener ขยาย SimpleOnGestureListener โดยใช้ onFling diffX/diffY กำหนดทิศทาง SWIPE_THRESHOLD (100px) และ SWIPE_VELOCITY_THRESHOLD (100 px/s) คือเกณฑ์ การใช้งาน: view.setOnTouchListener(OnSwipeTouchListener(context).apply { onSwipeLeft = { dismiss() } })
เพิ่มปุ่มลบเมื่อปัดซ้ายบนเซลล์ตาราง ใช้ API ในตัวของ iOS 11+ โดยไม่ต้องรู้จำท่าทางด้วยตนเอง
func tableView(
_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(
style: .destructive,
title: "ลบ"
) { _, _, completionHandler in
items.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
completionHandler(true)
}
deleteAction.backgroundColor = .systemRed
let config = UISwipeActionsConfiguration(actions: [deleteAction])
config.performsFirstActionWithFullSwipe = true
return config
}
เมธอดส่งคืน UISwipeActionsConfiguration พร้อมอาร์เรย์ของ UIContextualAction style: .destructive กำหนดพื้นหลังสีแดงโดยอัตโนมัติ performsFirstActionWithFullSwipe = true อนุญาตให้ดำเนินการด้วยการปัดเต็มโดยไม่ต้องแตะเพิ่มเติม ที่ IT Sectr เราใช้ API นี้สำหรับทุกรายการที่มีการกระทำ — เป็นมาตรฐาน รองรับ VoiceOver และไม่ต้องใช้ GestureRecognizer แบบกำหนดเอง
คำถามที่พบบ่อย
การปัดเป็นท่าทาง ไม่ต่อเนื่อง ที่มีการเคลื่อนที่เร็วและยกนิ้ว การเลื่อนคือการลากต่อเนื่องที่มีการติดตามพิกัด UISwipeGestureRecognizer ทำงานครั้งเดียว UIPanGestureRecognizer สร้าง .began → .changed → .ended การเลือกขึ้นอยู่กับสถานการณ์: การปัดสำหรับการกระทำ (ลบ พลิก) pan สำหรับการเคลื่อนที่ (แผนที่ ตัวเลื่อน)
ตรรกะของท่าทางเหมือนกัน แต่ API ต่างกัน iOS ใช้ UISwipeGestureRecognizer กับ direction Android ใช้ GestureDetector.onFling() กับเกณฑ์แบบกำหนดเอง ผลลัพธ์คือ UX เดียวกันกับการใช้งานที่ต่างกัน เฟรมเวิร์กข้ามแพลตฟอร์ม (Flutter, React Native) สรุปความแตกต่างผ่าน API การปัดแบบรวม
ใน iOS (11+) ใช้ UISwipeActionsConfiguration ใน UITableViewDelegate ใน Android ใช้ ItemTouchHelper.SimpleCallback สำหรับ RecyclerView API เหล่านี้แทนที่ GestureRecognizer แบบกำหนดเอง รองรับปุ่มการกระทำและแอนิเมชัน จัดการการขัดแย้งกับการเลื่อนอย่างถูกต้อง และปรับให้เข้ากับ HIG ของแพลตฟอร์ม
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม