Custom Gestures in Android: Implementing Swipe and Pinch using GestureDetector in Kotlin

In Android development, creating intuitive and responsive user interfaces often involves implementing custom gestures. Gestures such as swipe and pinch-to-zoom enhance user interaction and provide a more natural feel to applications. While Jetpack Compose offers built-in gesture support, many existing Android applications are still built using the traditional XML layout system and Kotlin. This blog post explores how to implement custom gestures like swipe and pinch using GestureDetector in Kotlin within an XML-based Android project.

Understanding GestureDetector

GestureDetector is a utility class provided by the Android framework that helps detect various gestures, such as single taps, double taps, flings (swipes), long presses, and more. It interprets touch events and notifies a listener that you define with specific gesture events. Using GestureDetector simplifies the process of gesture recognition by abstracting the complex logic of analyzing touch events.

Why Use GestureDetector?

  • Gesture Recognition: Provides built-in support for common gestures, reducing boilerplate code.
  • Customizable: Allows developers to define custom behavior for specific gestures.
  • Compatibility: Works seamlessly with traditional Android XML layouts and Kotlin code.

Implementing Custom Gestures: Swipe and Pinch

To demonstrate the implementation of custom gestures, we’ll focus on swipe and pinch-to-zoom gestures. First, set up your Android project with an XML layout and a Kotlin Activity.

Step 1: Set Up the Layout XML

Create a simple XML layout with a View that will detect gestures:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/gestureView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerInside"
        android:src="@drawable/your_image" />

</RelativeLayout>

In this layout, gestureView is an ImageView that will respond to our custom gestures. Replace @drawable/your_image with your image resource.

Step 2: Implement GestureDetector in Kotlin Activity

In your Kotlin Activity, implement GestureDetector and the necessary gesture listeners.


import android.annotation.SuppressLint
import android.os.Bundle
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GestureDetectorCompat

class MainActivity : AppCompatActivity() {

    private lateinit var gestureView: ImageView
    private lateinit var gestureDetector: GestureDetectorCompat
    private lateinit var scaleGestureDetector: ScaleGestureDetector

    private var scaleFactor = 1.0f

    @SuppressLint("ClickableViewAccessibility")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        gestureView = findViewById(R.id.gestureView)

        // Setup Gesture Detector for Swipe Gestures
        gestureDetector = GestureDetectorCompat(this, MyGestureListener())

        // Setup Scale Gesture Detector for Pinch-to-Zoom
        scaleGestureDetector = ScaleGestureDetector(this, ScaleListener())

        gestureView.setOnTouchListener { _, event ->
            gestureDetector.onTouchEvent(event)
            scaleGestureDetector.onTouchEvent(event)
            true
        }
    }

    // Inner class for handling Swipe Gestures
    inner class MyGestureListener : GestureDetector.SimpleOnGestureListener() {
        private val SWIPE_THRESHOLD = 100
        private val SWIPE_VELOCITY_THRESHOLD = 100

        override fun onDown(event: MotionEvent): Boolean {
            return true
        }

        override fun onFling(
            e1: MotionEvent?,
            e2: MotionEvent,
            velocityX: Float,
            velocityY: Float
        ): Boolean {
            if (e1 == null) return false

            val diffX = e2.x - e1.x
            val diffY = e2.y - e1.y

            if (Math.abs(diffX) > Math.abs(diffY) &&
                Math.abs(diffX) > SWIPE_THRESHOLD &&
                Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
                // Right or left swipe
                if (diffX > 0) {
                    onSwipeRight()
                } else {
                    onSwipeLeft()
                }
                return true
            }
            return super.onFling(e1, e2, velocityX, velocityY)
        }
    }

    // Inner class for handling Pinch-to-Zoom Gesture
    inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            scaleFactor *= detector.scaleFactor
            scaleFactor = Math.max(0.1f, Math.min(scaleFactor, 5.0f)) // Limit zoom
            gestureView.scaleX = scaleFactor
            gestureView.scaleY = scaleFactor
            return true
        }
    }

    // Swipe Gesture Actions
    private fun onSwipeLeft() {
        // Handle swipe left action
    }

    private fun onSwipeRight() {
        // Handle swipe right action
    }
}

Step 3: Explanation of the Code

  1. Initialization:
    • gestureView: An ImageView instance, findViewById to map layout View.
    • gestureDetector: An instance of GestureDetectorCompat to handle swipe gestures.
    • scaleGestureDetector: An instance of ScaleGestureDetector to handle pinch-to-zoom gestures.
    • scaleFactor: A float variable used for image zoom scaling with an initial value of 1.0f.
  2. GestureDetector for Swipes (MyGestureListener):
    • onDown(event: MotionEvent):
      • Ensures that the GestureDetector starts listening for gestures when the user touches the screen.
    • onFling(...):
      • Detects swipe gestures.
      • SWIPE_THRESHOLD and SWIPE_VELOCITY_THRESHOLD are used to determine if the gesture is a valid swipe.
      • If a swipe is detected, onSwipeLeft() or onSwipeRight() are called.
  3. ScaleGestureDetector for Pinch-to-Zoom (ScaleListener):
    • onScale(detector: ScaleGestureDetector):
      • Detects pinch-to-zoom gestures and scales the gestureView accordingly.
      • scaleFactor limits the zoom level between 0.1f and 5.0f.
  4. Set OnTouchListener:
    • We setup setOnTouchListener to dispatch the event for both the Swipe and ScaleGestureDetector for event detection, setting to true so the events don’t get propogated further.
  5. Action Methods (onSwipeLeft(), onSwipeRight()):
    • Handle the respective swipe actions (e.g., navigate to the previous/next image).

Complete Code Example

MainActivity.kt


import android.annotation.SuppressLint
import android.os.Bundle
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GestureDetectorCompat

class MainActivity : AppCompatActivity() {

    private lateinit var gestureView: ImageView
    private lateinit var gestureDetector: GestureDetectorCompat
    private lateinit var scaleGestureDetector: ScaleGestureDetector

    private var scaleFactor = 1.0f

    @SuppressLint("ClickableViewAccessibility")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        gestureView = findViewById(R.id.gestureView)

        // Setup Gesture Detector for Swipe Gestures
        gestureDetector = GestureDetectorCompat(this, MyGestureListener())

        // Setup Scale Gesture Detector for Pinch-to-Zoom
        scaleGestureDetector = ScaleGestureDetector(this, ScaleListener())

        gestureView.setOnTouchListener { _, event ->
            gestureDetector.onTouchEvent(event)
            scaleGestureDetector.onTouchEvent(event)
            true
        }
    }

    // Inner class for handling Swipe Gestures
    inner class MyGestureListener : GestureDetector.SimpleOnGestureListener() {
        private val SWIPE_THRESHOLD = 100
        private val SWIPE_VELOCITY_THRESHOLD = 100

        override fun onDown(event: MotionEvent): Boolean {
            return true
        }

        override fun onFling(
            e1: MotionEvent?,
            e2: MotionEvent,
            velocityX: Float,
            velocityY: Float
        ): Boolean {
            if (e1 == null) return false

            val diffX = e2.x - e1.x
            val diffY = e2.y - e1.y

            if (Math.abs(diffX) > Math.abs(diffY) &&
                Math.abs(diffX) > SWIPE_THRESHOLD &&
                Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
                // Right or left swipe
                if (diffX > 0) {
                    onSwipeRight()
                } else {
                    onSwipeLeft()
                }
                return true
            }
            return super.onFling(e1, e2, velocityX, velocityY)
        }
    }

    // Inner class for handling Pinch-to-Zoom Gesture
    inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            scaleFactor *= detector.scaleFactor
            scaleFactor = Math.max(0.1f, Math.min(scaleFactor, 5.0f)) // Limit zoom
            gestureView.scaleX = scaleFactor
            gestureView.scaleY = scaleFactor
            return true
        }
    }

    // Swipe Gesture Actions
    private fun onSwipeLeft() {
        Toast.makeText(this, "Swiped Left", Toast.LENGTH_SHORT).show()
    }

    private fun onSwipeRight() {
        Toast.makeText(this, "Swiped Right", Toast.LENGTH_SHORT).show()
    }
}

activity_main.xml


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/gestureView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerInside"
        android:src="@drawable/sample_image" />

</RelativeLayout>

Conclusion

Implementing custom gestures such as swipe and pinch in Android using GestureDetector allows developers to create more interactive and intuitive user experiences. This approach provides a straightforward way to recognize common gestures within traditional XML layouts and Kotlin code, making it suitable for existing and new Android applications. By customizing the gesture detection and response, developers can tailor the application’s behavior to specific user needs, enhancing overall usability.