Những điểm chính
Vuốt là chuyển động trượt nhanh của ngón tay trên bề mặt màn hình, kết thúc bằng việc nhấc ngón tay lên. Vuốt có thể ngắn (lật qua một phần tử) hoặc dài (mở rèm), nhưng đặc điểm chính là tính rời rạc: hệ thống nhận dạng nó như một sự kiện đơn lẻ, không phải một chuỗi tọa độ liên tục.
Hệ điều hành di động phát hiện vuốt dựa trên ba tham số: khoảng cách tối thiểu (ngưỡng), tốc độ tối thiểu (velocity) và hướng. Nếu ngón tay di chuyển chậm hoặc khoảng cách không đủ, hệ thống diễn giải cử chỉ là pan hoặc chạm. Giá trị ngưỡng khác nhau giữa các nền tảng: iOS sử dụng hằng số trong UISwipeGestureRecognizer với ngưỡng cố định, Android sử dụng ViewConfiguration.getScaledTouchSlop() trả về ~8–16 dp tùy theo mật độ màn hình.
Theo Tài liệu dành cho nhà phát triển Android, onFling (vuốt) trong GestureDetector kích hoạt ở tốc độ trên 100 px/s và khoảng cách từ 50px. Tại IT Sectr, chúng tôi sử dụng vuốt cho điều hướng (thư viện ảnh, giao diện thẻ, hướng dẫn) — cử chỉ này trực quan cho người dùng mọi lứa tuổi.
Vuốt và pan (pan/drag) là các loại cử chỉ khác nhau, mặc dù cả hai đều liên quan đến việc di chuyển ngón tay trên màn hình. Vuốt là rời rạc và có nghĩa là "thực hiện một hành động", trong khi pan là liên tục và có nghĩa là "di chuyển một đối tượng khi ngón tay còn trên màn hình".
| Tham số | Vuốt (Swipe) | Pan/Kéo (Pan/Drag) |
|---|---|---|
| Loại | Rời rạc | Liên tục |
| Sự kiện | Đơn lẻ (được nhận dạng/không được nhận dạng) | Nhiều (.began → .changed → .ended) |
| Tốc độ | Cao (chuyển động nhanh) | Bất kỳ (chuyển động chậm) |
| Ví dụ iOS | UISwipeGestureRecognizer | UIPanGestureRecognizer |
| Ví dụ Android | GestureDetector.onFling() | GestureDetector.onScroll() |
| Kịch bản | Xóa email, lật trang | Di chuyển bản đồ, kéo tệp |
| Theo dõi tọa độ | Không (chỉ hướng) | Có (cập nhật tọa độ liên tục) |
Sự lựa chọn giữa vuốt và pan phụ thuộc vào nhiệm vụ. Cho các hành động trên một phần tử (xóa, lưu trữ, mở menu) hãy dùng vuốt. Cho thao tác trực tiếp (di chuyển, thu phóng) hãy dùng pan. Trộn lẫn cử chỉ trên cùng một phần tử yêu cầu require(toFail:) hoặc logic tùy chỉnh.
UISwipeGestureRecognizer là một lớp con của UIGestureRecognizer nhận dạng vuốt rời rạc theo một trong bốn hướng. Không giống UIPanGestureRecognizer, nó không tạo ra các sự kiện .changed trung gian: khi được nhận dạng, hành động được gọi chính xác một lần.
Thuộc tính direction (UISwipeGestureRecognizer.Direction) thiết lập hướng theo dõi — .left, .right, .up, .down. Có thể kết hợp các hướng qua mặt nạ: [.left, .right] cho vuốt ngang. Thuộc tính numberOfTouchesRequired xác định số ngón tay — mặc định là 1. UISwipeGestureRecognizer không có ngưỡng khoảng cách có thể cấu hình — iOS sử dụng giá trị tích hợp khoảng 20px, đủ cho nhận dạng đáng tin cậy.
Vì UISwipeGestureRecognizer là bộ nhận dạng rời rạc, nó không xung đột với UIPanGestureRecognizer: chuyển động chậm kích hoạt pan, chuyển động nhanh kích hoạt swipe. Tuy nhiên, khi cùng tồn tại với UITapGestureRecognizer trên cùng một view, có thể cần require(toFail:) để ngăn nhận dạng chạm sai.
Trong Android, API chính cho nhận dạng vuốt là GestureDetector với callback OnGestureListener, cụ thể là phương thức onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY). GestureDetector phân tích vị trí ban đầu và cuối cùng của ngón tay, tốc độ di chuyển trên các trục và khoảng cách, sau đó gọi onFling khi vượt quá ngưỡng.
Các ngưỡng tối thiểu có thể cấu hình qua ViewConfiguration: VelocityTracker.getMinVelocity() (~100 px/s) và ViewConfiguration.getScaledTouchSlop() (~8–16 dp). OnSwipeTouchListener tùy chỉnh là một mẫu phổ biến giúp đơn giản hóa API: nhà phát triển không cần tính toán thủ công hướng và tốc độ. Android Support Library không chứa "SwipeDetector" tích hợp, vì vậy một wrapper tùy chỉnh là giải pháp tiêu chuẩn.
Cho vuốt trong danh sách, Google khuyến nghị sử dụng ItemTouchHelper.SimpleCallback từ RecyclerView, nó trừu tượng hóa việc phát hiện cử chỉ, hoạt ảnh và callback onSwiped(). Tại IT Sectr, chúng tôi sử dụng SwipeListener tùy chỉnh cho ViewPager2 và giao diện thẻ — điều này cho phép kiểm soát độ nhạy vuốt mà không ảnh hưởng đến hiệu suất.
Cho danh sách (UITableView, RecyclerView), Apple và Google cung cấp các API chuyên biệt thay thế việc thêm thủ công UISwipeGestureRecognizer. Các API này hỗ trợ nút hành động, hoạt ảnh tùy chỉnh và cử chỉ "vuốt để xóa" — các mẫu tiêu chuẩn trong ứng dụng di động.
Trong iOS (11+), UISwipeActionsConfiguration được sử dụng: phương thức tableView(_:leadingSwipeActionsConfigurationForRowAt:) trả về một mảng UIContextualAction — các nút có tiêu đề, kiểu (destructive, normal) và trình xử lý. Ví dụ: "Xóa" — .destructive với nền đỏ, "Lưu trữ" — .normal với nền xám. Hệ thống tạo hoạt ảnh cho sự xuất hiện của các nút và gọi khối completion sau khi chạm.
Trong Android, ItemTouchHelper.SimpleCallback được gắn vào RecyclerView qua ItemTouchHelper.attachToRecyclerView(). Các tham số bao gồm cờ hướng (start, end, up, down) và callback onSwiped(viewHolder, direction). Bên trong onSwiped, adapter.notifyItemRemoved(position) được gọi để xóa phần tử với hoạt ảnh. ItemTouchHelper tự động xử lý cử chỉ, xung đột cuộn và trả lại phần tử nếu vuốt chưa hoàn thành. Theo Google I/O 2024, ItemTouchHelper được sử dụng trong 70% ứng dụng Android có hành động vuốt trong danh sách.
Thêm UISwipeGestureRecognizer vào view để quay lại màn hình trước. Mã cho thấy cấu hình tối thiểu của bộ nhận dạng với hướng .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)
}
}
}
Bộ nhận dạng được gắn vào view gốc của bộ điều khiển. Việc kiểm tra navigationController.viewControllers.count đảm bảo pop chỉ được thực hiện khi có màn hình trước trong ngăn xếp. Cho bộ điều khiển modal, dismiss được sử dụng. Mã không yêu cầu kiểm tra trạng thái — UISwipeGestureRecognizer là rời rạc và chỉ gọi hành động khi nhận dạng thành công.
Triển khai một wrapper GestureDetector đơn giản xác định hướng vuốt và gọi 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 mở rộng SimpleOnGestureListener, triển khai onFling. diffX/diffY xác định hướng, SWIPE_THRESHOLD (100px) và SWIPE_VELOCITY_THRESHOLD (100 px/s) là các ngưỡng. Cách dùng: view.setOnTouchListener(OnSwipeTouchListener(context).apply { onSwipeLeft = { dismiss() } }).
Thêm nút xóa khi vuốt trái trên một ô bảng. Sử dụng API tích hợp iOS 11+ mà không cần nhận dạng cử chỉ thủ công.
func tableView(
_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(
style: .destructive,
title: "Xóa"
) { _, _, 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
}
Phương thức trả về UISwipeActionsConfiguration với một mảng UIContextualAction. style: .destructive tự động đặt nền đỏ. performsFirstActionWithFullSwipe = true cho phép thực hiện hành động bằng một lần vuốt đầy đủ mà không cần chạm thêm. Tại IT Sectr, chúng tôi sử dụng API này cho tất cả danh sách có hành động — nó được chuẩn hóa, hỗ trợ VoiceOver và không yêu cầu GestureRecognizer tùy chỉnh.
Câu hỏi thường gặp
Vuốt là cử chỉ rời rạc với chuyển động nhanh và nhấc ngón tay, pan là kéo liên tục với theo dõi tọa độ. UISwipeGestureRecognizer kích hoạt một lần, UIPanGestureRecognizer tạo ra .began → .changed → .ended. Lựa chọn phụ thuộc vào kịch bản: vuốt cho hành động (xóa, lật), pan cho di chuyển (bản đồ, thanh trượt).
Logic cử chỉ giống hệt nhau, nhưng API khác nhau. iOS sử dụng UISwipeGestureRecognizer với direction. Android sử dụng GestureDetector.onFling() với ngưỡng tùy chỉnh. Kết quả là UX giống nhau với các triển khai khác nhau. Các framework đa nền tảng (Flutter, React Native) trừu tượng hóa sự khác biệt qua một API vuốt hợp nhất.
Trong iOS (11+), sử dụng UISwipeActionsConfiguration trong UITableViewDelegate. Trong Android, sử dụng ItemTouchHelper.SimpleCallback cho RecyclerView. Các API này thay thế GestureRecognizer thủ công, hỗ trợ nút hành động và hoạt ảnh, xử lý chính xác xung đột cuộn và thích ứng với HIG nền tảng.
Tổng kết
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.
Đọc thêm