Skip to content

fix(study): the typing card — keyboard on the front, mid-flip stutter, and a card that jumped - #235

Merged
jvsena42 merged 7 commits into
mainfrom
fix/study-typing-keyboard-on-front
Sep 4, 2026
Merged

fix(study): the typing card — keyboard on the front, mid-flip stutter, and a card that jumped#235
jvsena42 merged 7 commits into
mainfrom
fix/study-typing-keyboard-on-front

Conversation

@jvsena42

@jvsena42 jvsena42 commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Four fixes to the study screen on a deck with Type the answer on. Each was measured on emulator-5554; the numbers are in journeys/RESULTS.md.

1. The keyboard came up over the next card's front

AnimatedContent keeps the outgoing card composed for its fade and re-runs the content lambda while it does, so the input read state.typePhase — by then already Answering for the card that had just arrived. The outgoing card drew the incoming card's input and its FocusRequester fired. CardSnapshot now carries the card's own phase and typed text.

8 of 8 advances → 0 of 19.

2. The turn stuttered halfway through

Not the card. The field took focus the instant the back face composed — the frame the rotation crosses 90° — and showing the IME makes SurfaceFlinger allocate its window surface. atrace over one flip:

460 ms  putmethod.latin   Choreographer#doFrame
446 ms  putmethod.latin   draw-VRI[InputMethod]
426 ms  RenderThread      dequeueBuffer / allocateHelper
433 ms  surfaceflinger    present
425 ms  jvsena42.loopky   eglSwapBuffersWithDamageKHR   <- us, blocked

The field now waits out the turn before asking for focus. Over the 750 ms of a flip: 21 frames → 44 (it was dropping every other one), janky 13.6% → 7.1%, p90 32 ms → 16 ms. The IME's ~445 ms is unchanged and unavoidable — it is now spent where it reads as the keyboard arriving.

Seven other explanations were built and benched first, and all seven were wrong: a height animation on the rows below the card, composing the back face at the crossing, TextAutoSize, the rounded clip outside the 3D layer, rotationY itself, the reveal haptic, the card's ripple.

3. The card jumped when the grades appeared

The card is weight(1f) of a column the screen padded for the IME, so the keyboard's departure resized it — right as a checked answer brought the grades in. Its y ran 405 → 579 → 405 across front / answering / graded, every word re-flowing through TextAutoSize on the way. That is the "flash": no dropped frames (3.2% janky either way), the whole layout moving in one frame.

The screen no longer pads for the IME. The card keeps its height and its lower edge passes behind the keyboard; the input block sits at the card's middle, clear of it (controls at 953..1434 against a keyboard top of 1524), and still scrolls where it would not be.

Card y after: 405..1875 in all three states. Same in landscape at w914dp (332..869), which also makes typing usable there for the first time — the card had been a ~40 px strip with its controls unreachable.

4. Two hard cuts, softened

The miss line grows in rather than appearing under the finger already reaching for Check.

Verified

Journey 03, extended with a typing block and the two checks these fixes would have failed (frames per turn; study_card bounds identical across states). Portrait and landscape, detektAll and assembleDebug green. journeys/RESULTS.md records the runs, including the control experiment — tab switches at 1–4% janky against the flip's 13% — that turned this from guessing into measuring.

🤖 Generated with Claude Code

jvsena42 and others added 7 commits September 4, 2026 18:47
`AnimatedContent` keeps the outgoing card composed for its fade and re-runs
the content lambda while doing so, so the input read `state.typePhase` —
by then already `Answering` for the card that had just arrived. The card on
its way out drew the *next* card's input, whose `FocusRequester` raised the
keyboard over a front with nothing to type into; it went away again when that
card was disposed a frame or two later, which is what made it look
intermittent. It reproduced on 8 of 8 advances on `emulator-5554`, twice for
several hundred milliseconds.

`CardSnapshot` now carries the card's own phase and typed text, so an outgoing
card can only ever draw its own — and `answerHidden` is derived from the phase
rather than passed alongside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ToGGVXht5QWNquT6RCK9n
Two hard cuts in the typing flow, both under the finger that caused them.

The grade row and flip hint collapse on a masked back, and `revealed` turns
true on the tap — so the card grew in one frame and then flipped over the next
700 ms, the jump arriving before the gesture it belongs to. The block is now
`BelowCardRows`, animating its own height (and keeping `ReviewingContent` under
the complexity ceiling the extra branch pushed it over).

The miss line sits above Check, so appearing in one frame moved the button out
from under a finger already reaching for it. It grows in instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ToGGVXht5QWNquT6RCK9n
Journey 03 gains the typing block it never had, ending on the check this bug
would have failed: poll `mInputShown` for a couple of seconds after a grade and
verify the keyboard never comes up over the next card's front.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ToGGVXht5QWNquT6RCK9n
The row block's height *is* the card's, so animating it re-measures the card
every frame — and the card re-measures `TextAutoSize` text, a full layout per
step down the size range. Spread over the 700 ms flip that showed up as a
stutter in the one animation the whole screen is built around: 23.6% janky
frames and a mean p95 of 140 ms over five flips on `emulator-5554`, against
14.1% and 59 ms with the animation gone.

The single-frame jump it was smoothing is the cheaper of the two. `BelowCardRows`
stays — the extraction is what keeps `ReviewingContent` inside the complexity
ceiling — and the comment now says why the swap has to remain a cut.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ToGGVXht5QWNquT6RCK9n
The stutter halfway through every flip on a typing deck was not the card. The
answer field takes focus the moment the back face composes — which is the frame
the rotation crosses 90° — and showing the IME makes SurfaceFlinger allocate its
window surface. `atrace` over one flip:

    460 ms  putmethod.latin   Choreographer#doFrame
    446 ms  putmethod.latin   draw-VRI[InputMethod]
    426 ms  RenderThread      dequeueBuffer / allocateHelper
    433 ms  surfaceflinger    present
    425 ms  jvsena42.loopky   eglSwapBuffersWithDamageKHR   <- us, blocked

So the field now waits out the rest of the turn before asking for focus, and the
keyboard comes up after the card lands instead of through it. Per flip on
`emulator-5554`, over the 750 ms of the turn: 21 frames -> 44 (the animation was
dropping every other frame), janky 13.6% -> 7.1%, p90 32 ms -> 16 ms. The ~445 ms
the IME costs is unchanged and unavoidable — it is now spent where it reads as
the keyboard arriving rather than as the card juddering.

Ruled out first, each by measurement rather than reasoning: the row block's
height animation (reverted separately), composing the back face at the crossing,
`TextAutoSize`, the rounded clip outside the 3D layer, `rotationY` itself, the
reveal haptic, and the card's ripple. A tab switch measured 1-4% janky against
the flip's 13% throughout, which is what said the cost was the flip's own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ToGGVXht5QWNquT6RCK9n
The card is `weight(1f)` of a column the screen was padding for the IME, so the
keyboard's arrival and departure resized it — and the departure lands exactly
when a checked answer opens the card and the grades appear. Measured on
`emulator-5554`, the card's y ran 405 -> 579 -> 405 across front, answering and
graded: a 174 px jump, with every word on the card re-flowing through
`TextAutoSize` as it went. That is the "flash" — it costs no dropped frames
(3.2% janky either way), it is the whole layout moving in one frame.

So the screen no longer pads for the IME. The card keeps its height and its
lower edge passes behind the keyboard; the input block sits at the card's middle,
which is clear of it (field/Check/Give up at 953..1434 against a keyboard top of
1524), and the block still scrolls for the windows where it would not be. The
rows under the card go back to being reserved in every state, since their height
is the card's and collapsing them was the other half of the same jump.

Card y across all three states, after: 405..1875 — identical. Same in landscape
at `w914dp`: 332..869 throughout, which also makes typing usable there for the
first time; the card had been squeezed to a ~40 px strip with its controls
unreachable.

`imePadding()` on the input block itself is not the smaller fix it looks like:
it pads by the keyboard's whole height rather than the overlapping part, which
shrinks that viewport below the block and shears Give up off the bottom — laid
out, hit-testable by neither finger nor tree, drawn nowhere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ToGGVXht5QWNquT6RCK9n
Journey 03 gains the two checks these fixes would have failed — frames per turn,
and `study_card`'s bounds identical across front, answering and graded — and
RESULTS.md carries the `atrace` that named the IME, along with the seven
explanations that were measured and wrong before it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ToGGVXht5QWNquT6RCK9n
@jvsena42 jvsena42 changed the title fix(study): keep the keyboard off the next card's front fix(study): the typing card — keyboard on the front, mid-flip stutter, and a card that jumped Sep 4, 2026
@jvsena42
jvsena42 merged commit e6038ed into main Sep 4, 2026
4 checks passed
@jvsena42
jvsena42 deleted the fix/study-typing-keyboard-on-front branch September 4, 2026 23:52
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.

1 participant