fix(study): the typing card — keyboard on the front, mid-flip stutter, and a card that jumped - #235
Merged
Merged
Conversation
`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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four fixes to the study screen on a deck with Type the answer on. Each was measured on
emulator-5554; the numbers are injourneys/RESULTS.md.1. The keyboard came up over the next card's front
AnimatedContentkeeps the outgoing card composed for its fade and re-runs the content lambda while it does, so the input readstate.typePhase— by then alreadyAnsweringfor the card that had just arrived. The outgoing card drew the incoming card's input and itsFocusRequesterfired.CardSnapshotnow 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.
atraceover one flip: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,rotationYitself, 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 throughTextAutoSizeon 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_cardbounds identical across states). Portrait and landscape,detektAllandassembleDebuggreen.journeys/RESULTS.mdrecords 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