Summary
The library has full atomic gestures (click, longClick, swipe, draw, pinch) but no composable building blocks for "press → move → hold → move → release" drag-and-drop flows. The natural primitives for this are missing — and there's no API-supported way to model one continuous pointer stream that includes a long-press, drag, dwell, and drop.
Motivating use case
A real-world cross-page drag-and-drop gesture in a HorizontalPager:
longPress(startTile) → drag-to-edge → dwell (~5s, pager auto-pages) → drag-to-drop on new page → settle → release
The downstream gesture detectors (long-press recognizer, drag state machine, per-event snap detection) all assume the gesture is one continuous pointer stream. Any finger lift between phases breaks the chain — long-press recognition resets, drag state clears, etc.
What's missing
click and longClick exist as full press-and-release atoms, but no "press-without-release" half-gestures:
| Production action |
Available today |
| Tap |
✅ click |
| Long-press + release |
✅ longClick |
| Swipe |
✅ swipe |
| Long-press, then drag |
❌ |
| Drag, hold, continue dragging |
❌ |
| Drag, settle, release |
❌ |
| Multi-touch hold |
✅ down + delay + up |
| Multi-touch with motion between presses |
❌ |
What I found trying to model this with 0.1.0 (and verified against main / 0.2.0-SNAPSHOT)
draw(path, duration) distributes time strictly by arc length × EaseInOutSine → a zero-length "hold" point gets zero time and is explicitly skipped at RealTouchRobotGestureScope.kt lines 192–196.
down() then draw() hits require(ongoingGesture == null) — hard crash at line 163.
down + delay + up is the official hold idiom, but it lifts the finger and ends the gesture.
- A multi-contour
Path in draw() produces a separate ACTION_DOWN/ACTION_UP per contour, not a continuous touch.
The workaround that does work is a vertical-only zigzag inside a single-contour draw() path, sized so each zigzag's arc length matches the desired hold time at the path's pixels-per-millisecond rate. It works but reads weirdly — and embeds a lot of arc-length / time-budget math in the test that's incidental to what we're trying to express.
Proposal — 4 new primitives
interface TouchRobotGestureScope {
// ... existing methods unchanged ...
/**
* Press [pointerId] down at [position] and hold for [duration] without lifting.
* Pair with [moveTo], [hold], or [up]. If [duration] is zero, sends ACTION_DOWN
* and returns immediately.
*/
suspend fun press(
position: IntOffset,
duration: Duration = Duration.ZERO,
pointerId: PointerId = PointerId(0),
)
/**
* [press] for `viewConfiguration.longPressTimeoutMillis + 100`. Mirrors
* [longClick]'s timing but does not release the finger.
*/
suspend fun longPress(position: IntOffset, pointerId: PointerId = PointerId(0))
/**
* Animate [pointerId] from its current position to [position] over [duration],
* sending ACTION_MOVE events on each frame (same easing as [draw]). Requires
* an active gesture for [pointerId].
*/
suspend fun moveTo(
position: IntOffset,
duration: Duration,
pointerId: PointerId = PointerId(0),
)
/**
* Hold [pointerId] stationary at its current position for [duration]. Sends
* no motion events. Requires an active gesture for [pointerId].
*/
suspend fun hold(duration: Duration, pointerId: PointerId = PointerId(0))
}
What the test would look like
Before (zigzag workaround):
draw(
path = Path().apply {
moveTo(start.x, start.y)
repeat(pressCycles) { zigzagCycle(start.x, start.y) }
lineTo(dwell.x, dwell.y)
repeat(holdCycles) { zigzagCycle(dwell.x, dwell.y) }
lineTo(drop.x, drop.y)
repeat(settleCycles) { zigzagCycle(drop.x, drop.y) }
},
duration = totalMs.milliseconds,
)
After:
longPress(start)
moveTo(dwell, 700.milliseconds)
hold(5500.milliseconds)
moveTo(drop, 700.milliseconds)
hold(400.milliseconds)
up()
Reads like the production gesture spec; loses all arc-length / cycle / amplitude bookkeeping.
Implementation cost looks small
All four primitives reuse infrastructure that's already present in RealTouchRobotGestureScope:
| New method |
Reuses |
press |
existing down + kotlinx.coroutines.delay |
longPress |
press + viewConfiguration.longPressTimeoutMillis (already used by longClick) |
moveTo |
the already-private dispatchMoveEventWithHistory + the time/easing loop from draw (extract) |
hold |
just kotlinx.coroutines.delay — no event dispatch |
The only structural decision is whether to:
- (a) Relax
draw's require(ongoingGesture == null) guard so down + draw + up works, or
- (b) Keep
draw self-contained and treat moveTo as the explicit "continue current gesture" entry point.
(b) feels cleaner — draw stays a "one-shot full gesture" primitive, moveTo is the building block.
Happy to send a PR if the API shape sounds right. Wanted to file the issue first to confirm direction.
🤖 Drafted with AI assistance based on reading the 0.1.0 / main source.
Summary
The library has full atomic gestures (
click,longClick,swipe,draw,pinch) but no composable building blocks for "press → move → hold → move → release" drag-and-drop flows. The natural primitives for this are missing — and there's no API-supported way to model one continuous pointer stream that includes a long-press, drag, dwell, and drop.Motivating use case
A real-world cross-page drag-and-drop gesture in a
HorizontalPager:The downstream gesture detectors (long-press recognizer, drag state machine, per-event snap detection) all assume the gesture is one continuous pointer stream. Any finger lift between phases breaks the chain — long-press recognition resets, drag state clears, etc.
What's missing
clickandlongClickexist as full press-and-release atoms, but no "press-without-release" half-gestures:clicklongClickswipedown + delay + upWhat I found trying to model this with 0.1.0 (and verified against
main/0.2.0-SNAPSHOT)draw(path, duration)distributes time strictly by arc length ×EaseInOutSine→ a zero-length "hold" point gets zero time and is explicitly skipped atRealTouchRobotGestureScope.ktlines 192–196.down()thendraw()hitsrequire(ongoingGesture == null)— hard crash at line 163.down + delay + upis the official hold idiom, but it lifts the finger and ends the gesture.Pathindraw()produces a separate ACTION_DOWN/ACTION_UP per contour, not a continuous touch.The workaround that does work is a vertical-only zigzag inside a single-contour
draw()path, sized so each zigzag's arc length matches the desired hold time at the path's pixels-per-millisecond rate. It works but reads weirdly — and embeds a lot of arc-length / time-budget math in the test that's incidental to what we're trying to express.Proposal — 4 new primitives
What the test would look like
Before (zigzag workaround):
After:
Reads like the production gesture spec; loses all arc-length / cycle / amplitude bookkeeping.
Implementation cost looks small
All four primitives reuse infrastructure that's already present in
RealTouchRobotGestureScope:pressdown+kotlinx.coroutines.delaylongPresspress+viewConfiguration.longPressTimeoutMillis(already used bylongClick)moveTodispatchMoveEventWithHistory+ the time/easing loop fromdraw(extract)holdkotlinx.coroutines.delay— no event dispatchThe only structural decision is whether to:
draw'srequire(ongoingGesture == null)guard sodown + draw + upworks, ordrawself-contained and treatmoveToas the explicit "continue current gesture" entry point.(b) feels cleaner —
drawstays a "one-shot full gesture" primitive,moveTois the building block.Happy to send a PR if the API shape sounds right. Wanted to file the issue first to confirm direction.
🤖 Drafted with AI assistance based on reading the 0.1.0 /
mainsource.