Handle multi-touch gestures

Try the Compose way
Jetpack Compose is the recommended UI toolkit for Android. Learn how to use touch and input in Compose.

A multi-touch gesture is when multiple pointers, such as fingers, touch the screen at the same time. This guide describes how to detect gestures that involve multiple pointers.

Refer to the following related resources:

Track multiple pointers

When multiple pointers touch the screen at the same time, the system generates the following touch events:

  • ACTION_DOWN: Starts the gesture when the first pointer touches the screen. This pointer is always at index 0 in the MotionEvent.
  • ACTION_POINTER_DOWN: Records extra pointers that enter the screen after the first. Obtain the index of each following pointer using getActionIndex().
  • ACTION_MOVE: Sent when a change has happened in a gesture to one or more pointers.
  • ACTION_POINTER_UP: Sent when a non-primary pointer goes up. Obtain the index of the pointer that just went up using getActionIndex() and check if FLAG_CANCELED is set.
  • ACTION_UP: Sent when the last pointer leaves the screen.
  • ACTION_CANCEL: Indicates that the entire gesture, including all pointers, is canceled.

Start and end gestures

A gesture is a series of events starting with an ACTION_DOWN event and ending with either an ACTION_UP or ACTION_CANCEL event. There is one active gesture at a time. The actions DOWN, MOVE, UP, and CANCEL apply to the entire gesture. For example, an event with ACTION_MOVE can indicate a movement for all pointers down at that moment.

Keep track of pointers

Use the pointer's index and ID to keep track of the individual pointers positions within a MotionEvent.

  • Index: A MotionEvent stores pointer information in an array. The index of a pointer is its position within this array. Most of the MotionEvent methods take the pointer index as a parameter, rather than the pointer ID.
  • ID: Each pointer also has an ID mapping that stays persistent across touch events to allow for tracking of an individual pointer across the entire gesture.

The order in which individual pointers appear within a motion event is undefined. Although the index of a pointer can change from one event to the next, the pointer ID is guaranteed to remain constant as long as the pointer remains active. Use the getPointerId() method to get the pointer's ID to track the pointer across all subsequent motion events in a gesture. Then for successive motion events, use the findPointerIndex() method to get the index of a given pointer ID in that motion event, as shown in the following example:

Kotlin

private var mActivePointerId: Int = 0

override fun onTouchEvent(event: MotionEvent): Boolean {
    ...
    // Get the pointer ID
    mActivePointerId = event.getPointerId(0)

    // ... Many touch events later...

    // Use the pointer ID to find the index of the active pointer
    // and fetch its position
    val (x: Float, y: Float) = event.findPointerIndex(mActivePointerId).let { pointerIndex ->
        // Get the pointer's current position
        event.getX(pointerIndex) to event.getY(pointerIndex)
    }
    ...
}

Java

private int mActivePointerId;

public boolean onTouchEvent(MotionEvent event) {
    ...
    // Get the pointer ID
    mActivePointerId = event.getPointerId(0);

    // ... Many touch events later...

    // Use the pointer ID to find the index of the active pointer
    // and fetch its position
    int pointerIndex = event.findPointerIndex(mActivePointerId);
    // Get the pointer's current position
    float x = event.getX(pointerIndex);
    float y = event.getY(pointerIndex);
    ...
}

To support multiple touch pointers, you can cache all active pointers with their IDs at their individual ACTION_POINTER_DOWN and ACTION_DOWN event time. Remove the pointers from your cache at their ACTION_POINTER_UP and ACTION_UPevents. You may find these cached IDs helpful to handle other action events correctly. For example, when processing an ACTION_MOVE event, find the index for each cached active pointer ID, retrieve the pointer's coordinates using the getX() and getY() functions, then compare these coordinates with your cached coordinates to discover which pointers moved.

Use the getActionIndex() function with ACTION_POINTER_UP and ACTION_POINTER_DOWN events only. Do not use this function with ACTION_MOVE events as this will always return 0.

Retrieve MotionEvent actions

Use the getActionMasked() method or the compatibility version MotionEventCompat.getActionMasked() to retrieve the action of a MotionEvent. Unlike the earlier getAction() method, getActionMasked() is designed to work with multiple pointers. It returns the action without the pointer indices. For actions with a valid pointer index, use getActionIndex() to return the index of the pointers associated with the action as shown in the following snippet:

Note: This example uses the MotionEventCompat class. This class is in the Support Library. Use the MotionEventCompat to provide the best support for a wide range of platforms. The MotionEventCompat is not a replacement for the MotionEvent class. Rather, it provides static utility methods to which you pass your MotionEvent object in order to receive the desired action associated with that event.

Kotlin

val (xPos: Int, yPos: Int) = MotionEventCompat.getActionMasked(event).let { action ->
    Log.d(DEBUG_TAG, "The action is ${actionToString(action)}")
    // Get the index of the pointer associated with the action.
    MotionEventCompat.getActionIndex(event).let { index ->
        // The coordinates of the current screen contact, relative to
        // the responding View or Activity.
        MotionEventCompat.getX(event, index).toInt() to MotionEventCompat.getY(event, index).toInt()
    }
}

if (event.pointerCount > 1) {
    Log.d(DEBUG_TAG, "Multitouch event")

} else {
    // Single touch event
    Log.d(DEBUG_TAG, "Single touch event")
}

...

// Given an action int, returns a string description
fun actionToString(action: Int): String {
    return when (action) {
        MotionEvent.ACTION_DOWN -> "Down"
        MotionEvent.ACTION_MOVE -> "Move"
        MotionEvent.ACTION_POINTER_DOWN -> "Pointer Down"
        MotionEvent.ACTION_UP -> "Up"
        MotionEvent.ACTION_POINTER_UP -> "Pointer Up"
        MotionEvent.ACTION_OUTSIDE -> "Outside"
        MotionEvent.ACTION_CANCEL -> "Cancel"
        else -> ""
    }
}

Java

int action = MotionEventCompat.getActionMasked(event);
// Get the index of the pointer associated with the action.
int index = MotionEventCompat.getActionIndex(event);
int xPos = -1;
int yPos = -1;

Log.d(DEBUG_TAG,"The action is " + actionToString(action));

if (event.getPointerCount() > 1) {
    Log.d(DEBUG_TAG,"Multitouch event");
    // The coordinates of the current screen contact, relative to
    // the responding View or Activity.
    xPos = (int)MotionEventCompat.getX(event, index);
    yPos = (int)MotionEventCompat.getY(event, index);

} else {
    // Single touch event
    Log.d(DEBUG_TAG,"Single touch event");
    xPos = (int)MotionEventCompat.getX(event, index);
    yPos = (int)MotionEventCompat.getY(event, index);
}
...

// Given an action int, returns a string description
public static String actionToString(int action) {
    switch (action) {

        case MotionEvent.ACTION_DOWN: return "Down";
	case MotionEvent.ACTION_MOVE: return "Move";
	case MotionEvent.ACTION_POINTER_DOWN: return "Pointer Down";
	case MotionEvent.ACTION_UP: return "Up";
	case MotionEvent.ACTION_POINTER_UP: return "Pointer Up";
	case MotionEvent.ACTION_OUTSIDE: return "Outside";
	case MotionEvent.ACTION_CANCEL: return "Cancel";
    }
    return "";
}

For more information about multi-touch and additional examples, see Drag and scale.