fix(scaffold): keep bars visible on non-scrollable lists
The DisappearingScaffold hid its top/bottom bars purely from overscroll gestures, so a feed with too few items to scroll would end up with two empty bands where the chrome used to be. Pure overscroll (consumed.y == 0) from the fully-visible state is now ignored; once the bars have started moving we know the list is scrollable and edge overscroll keeps feeding bar motion as before.
This commit is contained in:
+12
@@ -36,6 +36,10 @@ import androidx.compose.ui.unit.Velocity
|
||||
* - onPostScroll reads `consumed.y + available.y` (the total scroll attempt that entered
|
||||
* the nested-scroll chain) and updates the bar offsets. Using the sum means the bars
|
||||
* also respond to overscroll attempts at the list edges.
|
||||
* - Pure overscroll (`consumed.y == 0`) from the fully-visible state is ignored. This
|
||||
* prevents short, non-scrollable lists from hiding the bars purely on an overscroll
|
||||
* gesture — which would leave blank padding at the top and bottom. Once the bars have
|
||||
* started moving (list is clearly scrollable), edge overscroll keeps affecting them.
|
||||
* - onPostFling snaps a mid-way bar to the nearest edge, using the fling's remaining
|
||||
* velocity as the spring's initial velocity so the settle feels continuous. No velocity
|
||||
* is returned upward to avoid phantom scrolls on parent containers.
|
||||
@@ -54,6 +58,14 @@ class DisappearingBarNestedScroll(
|
||||
val totalY = consumed.y + available.y
|
||||
if (totalY == 0f) return Offset.Zero
|
||||
|
||||
// If the list did not consume any scroll and the bars are fully visible, treat
|
||||
// this as a non-scrollable list and keep the bars in place. Without this, a tiny
|
||||
// feed would hide its chrome purely from overscroll gestures, leaving two empty
|
||||
// bands at the top and bottom where the bars used to be.
|
||||
val isPureOverscroll = consumed.y == 0f
|
||||
val barsFullyVisible = state.topHeightOffset == 0f && state.bottomHeightOffset == 0f
|
||||
if (isPureOverscroll && barsFullyVisible) return Offset.Zero
|
||||
|
||||
val deltaY = if (reverseLayout) -totalY else totalY
|
||||
applyDelta(deltaY)
|
||||
// Never consume: the content scrolls freely while the bars slide along.
|
||||
|
||||
+31
@@ -106,6 +106,37 @@ class DisappearingBarNestedScrollTest {
|
||||
assertEquals(-10f, state.bottomHeightOffset)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pure overscroll from fully-visible state leaves the bars alone`() {
|
||||
// A short list that cannot scroll: the LazyColumn consumes nothing and the whole
|
||||
// drag comes through as `available`. We should not hide the bars from this, or we
|
||||
// end up with two empty bands where the chrome used to be.
|
||||
val state = state()
|
||||
val connection = nsc(state)
|
||||
|
||||
connection.onPostScroll(Offset(0f, 0f), Offset(0f, -80f), NestedScrollSource.UserInput)
|
||||
|
||||
assertEquals(0f, state.topHeightOffset)
|
||||
assertEquals(0f, state.bottomHeightOffset)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pure overscroll still moves bars once they are already partially hidden`() {
|
||||
// Scrollable list that reached an edge: the list stops consuming but the user keeps
|
||||
// flinging. Because the bars are already mid-hide we know the list was scrolling,
|
||||
// so overscroll keeps feeding the bar motion — matching the "bars ride along"
|
||||
// philosophy.
|
||||
val state = state(topLimit = 100f, bottomLimit = 50f)
|
||||
state.topHeightOffset = -20f
|
||||
state.bottomHeightOffset = -20f
|
||||
val connection = nsc(state)
|
||||
|
||||
connection.onPostScroll(Offset(0f, 0f), Offset(0f, -40f), NestedScrollSource.UserInput)
|
||||
|
||||
assertEquals(-60f, state.topHeightOffset)
|
||||
assertEquals(-50f, state.bottomHeightOffset)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `canScroll returning false freezes the bars`() {
|
||||
val state = state()
|
||||
|
||||
Reference in New Issue
Block a user