# How to Implement Long-Press and Multi-Touch Handling for Special Keyboard Actions in Android

> Implement long-press and multi-touch for Android keyboard actions. Learn to detect gestures, track pointers, and enable complex inputs like flicking and cursor movement.

- Repository: [Kazu/japanesekeyboard](https://github.com/kazumaproject/japanesekeyboard)
- Tags: how-to-guide
- Published: 2026-03-05

---

**Implement long-press detection using per-pointer coroutine jobs with timeout delays, and handle multi-touch by tracking pointer IDs with `SparseArray` maps while suppressing conflicting gestures to enable complex keyboard interactions like flick input and cursor movement.**

The `kazumaproject/japanesekeyboard` repository demonstrates production-grade touch handling for custom Android keyboards. This article explains how to implement long-press and multi-touch handling for special keyboard actions using the actual source code from [`QWERTYKeyboardView.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/QWERTYKeyboardView.kt) and related components.

## Long-Press Detection Implementation

### The LongPressListener Interface

The foundation of long-press handling starts with a simple callback interface defined in [`LongPressListener.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/LongPressListener.kt):

```kotlin
interface LongPressListener {
    fun onLongPress(key: Key)
}

```

The keyboard view receives a concrete implementation—usually the IME service—that reacts to long-press events. This decoupling allows the view to remain agnostic about specific actions while the listener handles business logic.

### Per-Pointer Coroutine Management

`QWERTYKeyboardView` maintains a `SparseArray<Job>` called **`longPressJobs`** (lines 92‑93) to track active coroutines for each pointer:

```kotlin
private val longPressJobs = SparseArray<Job>()

```

Each pointer gets its own coroutine that delays for the system timeout before firing:

```kotlin
longPressJobs.put(pointerId, scope.launch {
    delay(longPressTimeout)           // system default timeout (lines 15‑16)
    longPressListener?.onLongPress(pressedKey.key)
    // Show popup, lock pointer, etc.
})

```

The job is cancelled immediately when the pointer lifts or a competing gesture (e.g., a flick) is detected via `cancelLongPressForPointer`.

### Key-Specific Long-Press Handling

Only specific keys trigger long-press actions without visual popups. The `longPressEnabledKeys` set (lines 49‑56) defines these:

```kotlin
private val longPressEnabledKeys = setOf(
    QWERTYKey.QWERTYKeyDelete,
    QWERTYKey.QWERTYKeySpace,
    QWERTYKey.QWERTYKeySwitchDefaultLayout,
    QWERTYKey.QWERTYKeyCursorLeft,
    QWERTYKey.QWERTYKeyCursorRight
)

```

During the callback, the view checks `if (key in longPressEnabledKeys)` to decide whether to execute the action directly or display a variation picker popup.

## Multi-Touch Management Strategies

### Pointer-to-View Mapping

The view tracks which key each finger presses using **`pointerButtonMap`**, a `SparseArray<View?>` (lines 90‑91):

```kotlin
private val pointerButtonMap = SparseArray<View?>()

```

This map enables independent tracking of multiple simultaneous key presses, ensuring each pointer ID correlates to its specific button view throughout the gesture lifecycle.

### First Finger Suppression

When a second finger touches down while the first is held, the first pointer is **suppressed** to prevent ambiguous gestures:

```kotlin
if (pointerButtonMap.size == 1) {
    val firstPointerId = pointerButtonMap.keyAt(0)
    // Dismiss preview, cancel long-press job...
    suppressedPointerId = firstPointerId
    pointerButtonMap.remove(firstPointerId)
}

```

*(lines 11‑27 in the `ACTION_POINTER_DOWN` block)*

The suppressed ID is ignored for subsequent move events (`if (pid == suppressedPointerId) continue`), allowing the second finger to take primary control without interference from the first finger's initial touch.

### Flick-Locking Mechanism

During flick gestures, the initiating pointer enters `flickLockedPointers` to prevent standard key-up handling:

```kotlin
if (!flickLockedPointers.contains(pointerId)) {
    // Normal release handling
}

```

*(lines 83‑85 in `ACTION_POINTER_UP`)*

This separation ensures that completing a flick gesture does not simultaneously trigger the underlying key's tap action. The set clears when the gesture finishes (`flickLockedPointers.clear()`).

### Pointer Start Coordinates

`pointerStartCoords` stores initial **(x, y)** positions for each pointer (lines 72‑73):

```kotlin
private val pointerStartCoords = SparseArray<Pair<Float, Float>>()

```

These coordinates enable:
- **Flick direction detection** via `detectFlickDirection`
- **Threshold validation** against `flickThreshold` to distinguish flicks from accidental movement

### Cursor Mode Handling

When `isCursorMode` is active, the view intercepts all motion events and translates them into directional cursor commands, bypassing standard long-press and flick detection:

```kotlin
if (isCursorMode) { 
    // Translate motion to cursor left/right/up/down
}

```

*(lines 66‑95)*

This special multi-touch state prioritizes arrow key simulation over character input gestures.

## Practical Implementation Example

Below is a minimal example showing how an IME service enables long-press and multi-touch handling using the JapaneseKeyboard components:

```kotlin
// Register the LongPressListener
keyboardView.setOnLongPressListener(object : LongPressListener {
    override fun onLongPress(key: Key) {
        when (key) {
            QWERTYKey.QWERTYKeyDelete -> {
                // Execute delete-all or show popup
                inputConnection.deleteSurroundingText(100, 0)
            }
            QWERTYKey.QWERTYKeySpace -> {
                // Insert full-width space
                inputConnection.commitText("　", 1)
            }
            QWERTYKey.QWERTYKeySwitchDefaultLayout -> {
                // Toggle between QWERTY and symbol layouts
                keyboardView.switchLayout()
            }
        }
    }
})

// Multi-touch is automatic - the view handles second-finger suppression
// via ACTION_POINTER_DOWN logic in QWERTYKeyboardView.kt

```

For tablet implementations, reference [`TabletKeyboardView.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/TabletKeyboardView.kt), which mirrors the same `longPressJob` and `pointerButtonMap` patterns with adjusted UI dimensions.

## Summary

- **Use `SparseArray<Job>`** to track per-pointer long-press coroutines, cancelling them immediately on pointer up or multi-touch detection.
- **Suppress the first pointer** when a second finger touches down to prevent conflicting gestures and accidental long-press triggers.
- **Implement flick-locking** via a pointer ID set to separate flick gestures from standard tap actions.
- **Check `isCursorMode`** before processing normal touch logic to enable directional arrow key simulation.
- **Reference [`LongPressListener.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/LongPressListener.kt)** for the callback contract and [`QWERTYKeyboardView.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/QWERTYKeyboardView.kt) for the complete touch pipeline implementation.

## Frequently Asked Questions

### How does the keyboard distinguish between a long-press and a flick gesture?

The system uses `pointerStartCoords` to track the initial touch position and calculates movement distance against `flickThreshold`. If the pointer moves beyond the threshold before `longPressTimeout` elapses, the long-press job is cancelled and the gesture is processed as a flick. The `flickLockedPointers` set then prevents the same pointer from triggering a key-up event once the flick completes.

### What happens when a user touches a second key while holding the first?

When `ACTION_POINTER_DOWN` detects a second finger, the view identifies the first pointer ID from `pointerButtonMap`, cancels its long-press job via `cancelLongPressForPointer`, and assigns it to `suppressedPointerId`. The first pointer is removed from `pointerButtonMap` and ignored in subsequent move events, allowing the second finger to control input without interference from the held key.

### Which keys support long-press actions without displaying a popup?

The `longPressEnabledKeys` set in [`QWERTYKeyboardView.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/QWERTYKeyboardView.kt) defines keys that execute actions directly: `QWERTYKeyDelete`, `QWERTYKeySpace`, `QWERTYKeySwitchDefaultLayout`, `QWERTYKeyCursorLeft`, and `QWERTYKeyCursorRight`. When these keys trigger `onLongPress`, the view performs the associated action immediately rather than showing a variation picker popup.

### How is cursor mode implemented differently from standard multi-touch?

When `isCursorMode` is true, `QWERTYKeyboardView` intercepts all motion events in the touch pipeline and translates X/Y movement into directional cursor commands (left, right, up, down). This mode bypasses the standard `longPressJobs` logic, `flickLockedPointers` checks, and key detection entirely, dedicating all touch processing to arrow key simulation until the mode is disabled.