diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScroll.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScroll.kt index acab585c4..9c426379f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScroll.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScroll.kt @@ -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. diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScrollTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScrollTest.kt index 791c54b79..aa8fd6e0a 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScrollTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingBarNestedScrollTest.kt @@ -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()