Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Android

Cách phát hiện cử chỉ vuốt (Fling Gesture) trên Android với GestureDetector

Giới thiệu

GestureDetector là class Android phát hiện các gesture phổ biến: tap, double tap, long press, scroll, fling (vuốt nhanh), v.v. Bài viết hướng dẫn implement fling detection để xử lý swipe left/right/up/down.

Các bước thực hiện

Bước 1: Tạo dự án mới

Android Studio → File → New Project → Empty View Activity.

Bước 2: Thiết kế giao diện (res/layout/activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:tools="https://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tvInstruction"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Vuốt màn hình (swipe) để test Fling"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tvResult"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:textSize="20sp"
        android:textColor="#2196F3"
        android:textStyle="bold"
        app:layout_constraintTop_toBottomOf="@id/tvInstruction"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Bước 3: Custom GestureListener (FlingDetector.kt)

package com.example.flinggesture

import android.view.GestureDetector
import android.view.MotionEvent
import android.widget.Toast

class FlingDetector(private val context: android.content.Context) : GestureDetector.SimpleOnGestureListener() {

    companion object {
        // Ngưỡng tối thiểu để coi là fling (pixel/giây)
        private const val SWIPE_THRESHOLD_VELOCITY = 100
        private const val SWIPE_MIN_DISTANCE = 100
    }

    override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
        val distanceX = e2.x - e1.x
        val distanceY = e2.y - e1.y

        // Kiểm tra ngưỡng khoảng cách và tốc độ
        if (Math.abs(distanceX) > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
            if (distanceX > 0) {
                onSwipeRight()
            } else {
                onSwipeLeft()
            }
            return true
        } else if (Math.abs(distanceY) > SWIPE_MIN_DISTANCE && Math.abs(velocityY) > SWIPE_THRESHOLD_VELOCITY) {
            if (distanceY > 0) {
                onSwipeDown()
            } else {
                onSwipeUp()
            }
            return true
        }
        return false
    }

    private fun onSwipeLeft() {
        showToast("Vuốt TRÁI (Swipe Left)")
    }

    private fun onSwipeRight() {
        showToast("Vuốt PHẢI (Swipe Right)")
    }

    private fun onSwipeUp() {
        showToast("Vuốt LÊN (Swipe Up)")
    }

    private fun onSwipeDown() {
        showToast("Vuốt XUỐNG (Swipe Down)")
    }

    private fun showToast(message: String) {
        Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
    }
}

Bước 4: MainActivity.kt - Kết nối GestureDetector

package com.example.flinggesture

import android.os.Bundle
import android.view.GestureDetector
import android.view.MotionEvent
import androidx.appcompat.app.AppCompatActivity
import com.example.flinggesture.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private lateinit var gestureDetector: GestureDetector

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Khởi tạo GestureDetector với custom listener
        gestureDetector = GestureDetector(this, FlingDetector(this))

        // Set touch listener cho root view (hoặc view muốn detect)
        binding.root.setOnTouchListener { _, event ->
            gestureDetector.onTouchEvent(event)
            true // Consume event
        }
    }

    // Override onTouchEvent của Activity cũng được (nếu không setOnTouchListener)
    // override fun onTouchEvent(event: MotionEvent): Boolean {
    //     return gestureDetector.onTouchEvent(event) || super.onTouchEvent(event)
    // }
}

Cách hiện đại: ViewCompat.setOnTouchListener + Kotlin Extension

Tạo extension function tái sử dụng:

// Extensions.kt
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.content.Context

fun View.onFling(
    minDistance: Int = 100,
    minVelocity: Int = 100,
    onSwipeLeft: () -> Unit = {},
    onSwipeRight: () -> Unit = {},
    onSwipeUp: () -> Unit = {},
    onSwipeDown: () -> Unit = {}
) {
    val detector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
        override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
            val dx = e2.x - e1.x
            val dy = e2.y - e1.y
            when {
                Math.abs(dx) > minDistance && Math.abs(velocityX) > minVelocity -> {
                    if (dx > 0) onSwipeRight() else onSwipeLeft()
                    return true
                }
                Math.abs(dy) > minDistance && Math.abs(velocityY) > minVelocity -> {
                    if (dy > 0) onSwipeDown() else onSwipeUp()
                    return true
                }
            }
            return false
        }
    })
    setOnTouchListener { _, event -> detector.onTouchEvent(event) }
}

Sử dụng:

binding.root.onFling(
    onSwipeLeft = { binding.tvResult.text = "← Left" },
    onSwipeRight = { binding.tvResult.text = "Right →" },
    onSwipeUp = { binding.tvResult.text = "↑ Up" },
    onSwipeDown = { binding.tvResult.text = "Down ↓" }
)

RecyclerView Swipe (ItemTouchHelper)

Nếu cần swipe trên item RecyclerView (xóa, archive...), dùng ItemTouchHelper:

val callback = object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
    override fun onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder): Boolean = false

    override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
        val position = viewHolder.adapterPosition
        when (direction) {
            ItemTouchHelper.LEFT -> { /* Archive */ }
            ItemTouchHelper.RIGHT -> { /* Delete */ }
        }
        adapter.notifyItemRemoved(position)
    }
}
ItemTouchHelper(callback).attachToRecyclerView(recyclerView)

ViewPager2 / ViewPager - Swipe ngang giữa trang

Để swipe chuyển trang (onboarding, tabs), dùng ViewPager2:


// build.gradle.kts
implementation("androidx.viewpager2:viewpager2:1.1.0")

// Layout


// Adapter
class ScreenSlideAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
    override fun getItemCount() = 3
    override fun createFragment(position: Int) = ScreenSlidePageFragment()
}

// Activity
binding.viewPager.adapter = ScreenSlideAdapter(this)

Lưu ý quan trọng

  • GestureDetector chỉ detect **single-pointer** gestures (1 ngón). Multi-touch dùng ScaleGestureDetector.
  • Fling khác với Scroll: Fling = vuốt nhanh rồi thả (velocity cao), Scroll = kéo chậm.
  • Ngưỡng SWIPE_MIN_DISTANCE và SWIPE_THRESHOLD_VELOCITY cần tune cho phù hợp UX.
  • Trên View có click listener: Fling có thể conflict với click. Cân nhắc dùng onSingleTapConfirmed cho click.
  • Jetpack Compose: Dùng Modifier.pointerInput { detectTapGestures { } } hoặc detectDragGestures { }.