Notice a row arriving, and stop subcomposing the list at the keyboard
Two faults from making the retained window lazy, both of them mine. A screen of blank between the last message and the box it was typed in. The window was recomputed when the view had moved far enough, and a message arriving does not move the view -- so the new row fell outside the window and stood in as a spacer of its guessed height. The version number the check compares against is only bumped by the recompute it guards, so asking before refreshing meant never noticing. It refreshes first now, and the window also watches how many rows there are, because a row arriving is the case it exists to catch and the one that does not announce itself through the scroll position. And the keyboard, which was the most expensive thing on the screen. The visible height came from a `BoxWithConstraints` wrapped around the transcript -- that is a `SubcomposeLayout`, and the IME animation changes the visible height on every frame of its slide, so the entire transcript was being subcomposed again for each of them. The same number read in the layout phase, from the scroll container's own measurement, makes it a relayout instead, and the rows keep the measurements they already have. Checked on the emulator: the newest message sits against the composer with the keyboard up, and there is no gap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2b24362cc4
commit
9052e5f55e
2 files changed
+29
-9
No files matched your search
@@ -8,7 +8,6 @@ import androidx.activity.result.PickVisualMediaRequest
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -1415,13 +1414,10 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: ()
|
|||||||
// is no frame in which the transcript is somewhere other than where it was left.
|
// is no frame in which the transcript is somewhere other than where it was left.
|
||||||
val settled = !restoring && !listState.settling
|
val settled = !restoring && !listState.settling
|
||||||
Box(Modifier.weight(1f).fillMaxWidth()) {
|
Box(Modifier.weight(1f).fillMaxWidth()) {
|
||||||
BoxWithConstraints(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
TranscriptColumn(
|
TranscriptColumn(
|
||||||
rows = rows,
|
rows = rows,
|
||||||
state = listState,
|
state = listState,
|
||||||
// A scrollable child is measured with no height bound, so the content cannot
|
|
||||||
// ask how tall the visible area is; this is the only place that knows.
|
|
||||||
viewportHeight = maxHeight,
|
|
||||||
contentPadding = TRANSCRIPT_PADDING,
|
contentPadding = TRANSCRIPT_PADDING,
|
||||||
spacing = TRANSCRIPT_SPACING,
|
spacing = TRANSCRIPT_SPACING,
|
||||||
modifier =
|
modifier =
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
@@ -18,6 +17,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.key
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -28,6 +28,7 @@ import androidx.compose.ui.draw.drawWithContent
|
|||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.layout
|
||||||
import androidx.compose.ui.layout.onPlaced
|
import androidx.compose.ui.layout.onPlaced
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
@@ -89,8 +90,13 @@ class TranscriptScroll(internal val scroll: ScrollState) {
|
|||||||
private var rowIndex = HashMap<Long, Int>()
|
private var rowIndex = HashMap<Long, Int>()
|
||||||
private var topsStale = true
|
private var topsStale = true
|
||||||
|
|
||||||
|
/** How many rows the list is drawing, so the window notices one arriving. */
|
||||||
|
var rowCount: Int by mutableIntStateOf(0)
|
||||||
|
private set
|
||||||
|
|
||||||
internal fun laidOut(order: List<Long>, spacing: Int, padTop: Int) {
|
internal fun laidOut(order: List<Long>, spacing: Int, padTop: Int) {
|
||||||
this.order = order
|
this.order = order
|
||||||
|
rowCount = order.size
|
||||||
this.spacing = spacing
|
this.spacing = spacing
|
||||||
this.padTop = padTop
|
this.padTop = padTop
|
||||||
rowIndex = HashMap(order.size)
|
rowIndex = HashMap(order.size)
|
||||||
@@ -286,10 +292,16 @@ class TranscriptScroll(internal val scroll: ScrollState) {
|
|||||||
* either side and this moves it in steps of two, so the margin absorbs the staleness.
|
* either side and this moves it in steps of two, so the margin absorbs the staleness.
|
||||||
*/
|
*/
|
||||||
internal fun trackRetained() {
|
internal fun trackRetained() {
|
||||||
|
// Before the comparison, not after it. Rows arriving is the case this exists to catch and
|
||||||
|
// it does not move the view: a message sent lands at the newest end, and if the range is
|
||||||
|
// not recomputed the new row is outside it and stands in as a spacer of its guessed height
|
||||||
|
// -- a screen of blank between the last message and the box it was typed in. The version
|
||||||
|
// it is compared against is only bumped by this call, so asking first meant never noticing.
|
||||||
|
refreshTops()
|
||||||
val viewportTop = scroll.maxValue - scroll.value
|
val viewportTop = scroll.maxValue - scroll.value
|
||||||
val step = (scroll.viewportSize * RETAIN_STEP_SCREENS).coerceAtLeast(1)
|
val step = (scroll.viewportSize * RETAIN_STEP_SCREENS).coerceAtLeast(1)
|
||||||
val moved = viewportTop - rangeAt
|
val moved = viewportTop - rangeAt
|
||||||
if (retained.isEmpty() || moved > step || moved < -step || topsVersion != rangeVersion) {
|
if (retained.isEmpty() || topsVersion != rangeVersion || moved > step || moved < -step) {
|
||||||
retained = retainedRange(viewportTop)
|
retained = retainedRange(viewportTop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,7 +422,6 @@ fun rememberTranscriptScroll(key: Any?): TranscriptScroll {
|
|||||||
fun TranscriptColumn(
|
fun TranscriptColumn(
|
||||||
rows: List<TranscriptRow>,
|
rows: List<TranscriptRow>,
|
||||||
state: TranscriptScroll,
|
state: TranscriptScroll,
|
||||||
viewportHeight: Dp,
|
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
spacing: Dp,
|
spacing: Dp,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -438,7 +449,20 @@ fun TranscriptColumn(
|
|||||||
modifier
|
modifier
|
||||||
.verticalScroll(state.scroll, reverseScrolling = true)
|
.verticalScroll(state.scroll, reverseScrolling = true)
|
||||||
.padding(contentPadding)
|
.padding(contentPadding)
|
||||||
.heightIn(min = viewportHeight)
|
// As tall as the visible area at least, so a conversation shorter than the screen sits
|
||||||
|
// against the composer rather than leaving a gap under it that cannot be scrolled away.
|
||||||
|
//
|
||||||
|
// Taken in the layout phase from the scroll container's own measurement, rather than
|
||||||
|
// from a `BoxWithConstraints` around this. That is a `SubcomposeLayout`, and the
|
||||||
|
// keyboard opening changes the visible height on every frame of its animation -- so
|
||||||
|
// the whole transcript was being subcomposed again for each of those frames, which is
|
||||||
|
// what made bringing the keyboard up cost more than anything else on the screen. Read
|
||||||
|
// here it is a relayout, and the rows keep the measurements they already have.
|
||||||
|
.layout { measurable, constraints ->
|
||||||
|
val placeable =
|
||||||
|
measurable.measure(constraints.copy(minHeight = state.scroll.viewportSize))
|
||||||
|
layout(placeable.width, placeable.height) { placeable.place(0, 0) }
|
||||||
|
}
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
// Once for the whole list, not once per row: this is where a saved position is put
|
// Once for the whole list, not once per row: this is where a saved position is put
|
||||||
// back, and by placement the scroll container's own measurements describe this layout.
|
// back, and by placement the scroll container's own measurements describe this layout.
|
||||||
|
|||||||
Reference in new issue
Block a user