Skip to content

Add moveTo() for continuing an active gesture#2

Open
comp615 wants to merge 1 commit into
saket:trunkfrom
comp615:add-moveto
Open

Add moveTo() for continuing an active gesture#2
comp615 wants to merge 1 commit into
saket:trunkfrom
comp615:add-moveto

Conversation

@comp615
Copy link
Copy Markdown

@comp615 comp615 commented Jun 3, 2026

Closes #1.

Summary

Adds a moveTo(position, duration, pointerId = PointerId(0)) primitive to TouchRobotGestureScope that animates an already-down pointer to a new position over a duration, dispatching only ACTION_MOVE events. This is the building block for composable multi-phase gestures (long-press → drag → hold → drag → release) where every phase must be one continuous pointer stream.

touchRobot.onRoot().performGesture {
  down(start)
  delay(viewConfiguration.longPressTimeoutMillis + 100.milliseconds)
  moveTo(dwell, 700.milliseconds)
  delay(5.seconds)               // ← stationary hold
  moveTo(drop, 700.milliseconds)
  up()
}

Scope decision: just moveTo, not all four primitives from #1

In the issue I proposed press / longPress / moveTo / hold. On re-reading the source I think only moveTo is actually missing — the other three are trivial compositions of API that's already public:

Sugar Equivalent today
press(pos, dur) down(pos); delay(dur)
longPress(pos) down(pos); delay(viewConfiguration.longPressTimeoutMillis + 100.milliseconds)
hold(dur) delay(dur) (literally identical — no event dispatch)

Whereas moveTo has no public equivalent — the per-frame easing loop lives inside draw() and is guarded by require(ongoingGesture == null). So this PR ships just the primitive that has no workaround. Happy to follow up with the sugar in a separate PR if you'd like it — easier to add than to remove from a fresh 0.1.0 API surface.

Implementation

draw()'s inner per-frame loop is extracted into a private animateAlongPaths() helper. Both draw() and moveTo() call it; draw() keeps owning its own down()/up() and contour iteration, while moveTo() reuses the existing ongoingGesture.downTime and a one-line Path from current → target position.

The helper takes two times to disambiguate them — motionDownTime for the MotionEvent.downTime field, motionStartTime for animation progress. They coincide for draw() (down + animate happen in the same tick) but differ for moveTo() (down happened earlier, possibly with a long hold in between).

Verification

  • All 7 existing Paparazzi snapshots are still byte-identical — the refactor is behavior-preserving. (Verified via ./gradlew check with no M on existing snapshot files.)
  • New Paparazzi test move to continues active pointer records a 4-second animation of down → wait (long-press) → moveTo → hold → moveTo → up against a pointerInput { … awaitPointerEvent() } consumer. The overlay text reads down=1 up=0 for the entire animation, proving the gesture stays continuous across the moveTo/delay chain.

API additions

interface TouchRobotGestureScope {
  // ... existing methods unchanged ...

  /**
   * Animate [pointerId] from its current position to [position] over [duration], dispatching
   * ACTION_MOVE events on each frame (with the same easing as [draw]).
   *
   * Unlike [swipe] and [draw], this does not send ACTION_DOWN or ACTION_UP events — it
   * continues an already-active gesture. […]
   */
  suspend fun moveTo(
    position: IntOffset,
    duration: Duration,
    pointerId: PointerId = PointerId(0),
  )
}

🤖 Drafted with AI assistance.

draw() owns the per-frame easing loop but guards itself with
require(ongoingGesture == null), so there's no public way to animate
an already-down pointer. This blocks composing multi-phase gestures
such as long-press → drag → hold → drag → release, where every phase
must be one continuous pointer stream.

Add a moveTo(position, duration, pointerId) primitive that animates
[pointerId] from its current position to [position] using the same
EaseInOutSine + frame-clock + historical-position machinery as draw().
It dispatches only ACTION_MOVE events — callers are responsible for
the surrounding down()/up().

Implementation extracts draw()'s inner per-frame loop into a private
animateAlongPaths() helper. draw() now calls it after its own down(),
and moveTo() calls it with the existing ongoingGesture.downTime. No
behaviour change for existing callers — all 7 existing Paparazzi
snapshots are still byte-identical.

Closes saket#1

Amp-Thread-ID: https://ampcode.com/threads/T-019e8e27-d50b-77b8-af50-adf21074e1f4
Co-authored-by: Amp <amp@ampcode.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Composable building blocks for continuous press → drag → hold → drop gestures

1 participant