State where a resized row goes instead of walking it there

Correcting by the error each pass could see, and asking again on the pass that
answered, was a frame per pass with the ones in between drawn: opening a card
visibly stepped. It also still missed, because two of the passes were spent
finding out what the list would do rather than telling it.

`requestScrollToItem` against the row itself says it outright, in one pass: the
row is placed wherever it has ended up and whether or not it is still on screen,
and a negative offset -- which is the list being asked for the rows below a card
that has just given the screen its whole height back -- is exactly what a close
needs and works. Everything else follows from where that puts it.

A call opened or shut *inside* a group is a third case, and it was being treated
as the second: the group is not the thing opening, it is the container, and
centring it on the tap threw a group of six the length of the screen. What keeps
the call under the finger is holding the group's top edge, so that everything
above the change -- that call's own heading included -- stays where it is.

Checked with ktfmtFormat, compileDebugKotlin, lintDebug and testDebugUnitTest,
and on the emulator against the sandbox with sampling as fast as the device will
report it, so an intermediate frame would show: a 2,785px card closed at 600,
1200 and 1800 lands centred on 600, 1200 and 1800 to the pixel, each in one
step; opening by the heading holds the heading still; and opening or closing a
call inside a group of five leaves the group's heading exactly where it was.
This commit is contained in:
iris-ai committed 2026-09-16 02:53:51 -04:00
1 parent 1b38579b97
commit 581e07624f
2 files changed
+73 -86

No files matched your search

+24 -17
View File
@@ -1190,8 +1190,9 @@ dev-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21).
One rule in two places (`AskedQuestion` and `PermissionAsk`). An answer
typed into **Other** matches no option, so that one is still written out.
- **Opening a row keeps still the end nearest the tap; closing one lands
the closed row centred on the tap itself** (2026-09-16). A row about to
open is small, so both its edges are within a heading's height of the
the closed row centred on the tap itself; and a call opened or shut
*inside* a group keeps the group's top edge** (2026-09-16). A row about
to open is small, so both its edges are within a heading's height of the
finger and the wanted one is the edge pressed: touch the upper half and
the top edge holds, so it opens downwards; touch the lower half and the
bottom edge holds, as the list does by default. A row about to close
@@ -1200,23 +1201,29 @@ dev-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21).
screen settles it, since both of its edges can be a screen's length from
the hand, and holding a *share* of the card's height was the same miss in
miniature: tap away from the middle of a long card and the heading landed
a card's height off. Where there is not enough conversation on the far
side to scroll, the row lands as close as the list can put it.
**The correction is measured, never predicted, and it is asked for from
a card's height off. The third case is the one where the row is not the
thing being opened at all but the container of it; what the reader is
looking at is the call under their finger, and holding the group's top
edge is what keeps everything above the change — that call's own heading
included — exactly where it is. Centring the group instead threw a group
of six the length of the screen. Where there is not enough conversation
on the far side to scroll, the row lands as close as the list can put it.
**The position is stated, not converged on, and it is asked for from
inside the list's own measure** (`settleHold`, called from
`TranscriptList`'s `afterMeasure`). Both halves of that were paid for:
working the scroll out from the change in height needs the list to behave
`TranscriptList`'s `afterMeasure`). Three things were paid for to get
there. A scroll asked for during the *placement* phase is never picked up
by another measure, so it does nothing whatever; from a coroutine it
arrives a frame late, which is the flick this mechanism exists to avoid.
Working the scroll out from the change in height needs the list to behave
the way the arithmetic assumed, and it does not — which item it holds
still across a resize, and how much of a scroll it can honour, depend on
what it has composed, and a row taller than the screen is anchored on
*itself*. And a scroll asked for during the *placement* phase is never
picked up by another measure, so it does nothing at all; from a coroutine
it arrives a frame late, which is the flick this mechanism exists to
avoid. So each pass asks for the error it can see, and the pass that
answers is where the rest becomes askable — three passes is the worst
case seen, including the one where the shut card has left the viewport
entirely and has to be asked back to the bottom edge before it can be
measured at all.
still across a resize depends on what it has composed, and a row taller
than the screen is anchored on *itself*. And correcting by the error a
pass can see, then asking again, is a *frame* per pass with the ones in
between drawn: opening a card visibly stepped. What is exact and takes
one pass is `requestScrollToItem` against the row itself, which places it
wherever it has ended up and whether or not it is still on screen — a
negative offset included, which is the list being asked for the rows
below a card that has just given the screen its whole height back.
- **The full-screen image lives on the screen, not in the row that drew
the thumbnail** (`SessionImageViewer`). A `Read` whose result is an image
is a row of one call until the next call arrives and makes it a group — a