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 5d2db649d..acab585c4 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 @@ -26,32 +26,38 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.unit.Velocity /** - * Single nested-scroll connection that hides/reveals the top and bottom bars together. + * Scroll-linked connection that hides/reveals the top and bottom bars together. * - * Behaviour: - * - onPreScroll: consumes the portion of the scroll delta used to move the bars - * (both hide and reveal), returning the exact amount absorbed so the list never - * "loses pixels" at the edge of the bars' travel range. - * - onPostFling: snaps a mid-way bar to the nearest edge, continuing the fling's - * tail velocity so the settle motion feels like part of the fling, not a second - * animation after it. No velocity is returned upward. + * Philosophy: the bars never consume scroll input. They simply ride along with the + * content — their offset changes at the same rate as the scroll, so the user keeps full + * control of the list with their finger. This mirrors the behaviour of Twitter, Instagram, + * Bluesky, etc., where content scrolling is never delayed by the chrome. + * + * - 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. + * - 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. */ class DisappearingBarNestedScroll( private val state: DisappearingBarState, private val canScroll: () -> Boolean, private val reverseLayout: Boolean, ) : NestedScrollConnection { - override fun onPreScroll( + override fun onPostScroll( + consumed: Offset, available: Offset, source: NestedScrollSource, ): Offset { if (!canScroll()) return Offset.Zero - if (available.y == 0f) return Offset.Zero + val totalY = consumed.y + available.y + if (totalY == 0f) return Offset.Zero - val deltaY = if (reverseLayout) -available.y else available.y - val applied = applyDelta(deltaY) - if (applied == 0f) return Offset.Zero - return Offset(0f, if (reverseLayout) -applied else applied) + val deltaY = if (reverseLayout) -totalY else totalY + applyDelta(deltaY) + // Never consume: the content scrolls freely while the bars slide along. + return Offset.Zero } override suspend fun onPostFling( @@ -59,36 +65,16 @@ class DisappearingBarNestedScroll( available: Velocity, ): Velocity { if (canScroll()) { - // Feed the fling's remaining velocity into the settle so the bar keeps - // moving in the same direction rather than starting a fresh animation. val velocityY = if (reverseLayout) -available.y else available.y state.settleToNearestEdge(initialVelocityY = velocityY) } - // Swallow any residual velocity so parents don't get a phantom fling kick. return Velocity.Zero } - /** - * Applies the given delta (in content-space – negative hides, positive reveals) to - * both bar offsets, clamped to their travel range. - * - * Returns the delta that was actually absorbed, using the side with the larger - * absorption so consumption reporting remains accurate when the two bars have - * different remaining travel. - */ - private fun applyDelta(deltaY: Float): Float { - val prevTop = state.topHeightOffset - val prevBottom = state.bottomHeightOffset + private fun applyDelta(deltaY: Float) { val topLimit = state.topHeightLimit val bottomLimit = state.bottomHeightLimit - - val newTop = (prevTop + deltaY).coerceIn(-topLimit, 0f) - val newBottom = (prevBottom + deltaY).coerceIn(-bottomLimit, 0f) - state.topHeightOffset = newTop - state.bottomHeightOffset = newBottom - - val topDelta = newTop - prevTop - val bottomDelta = newBottom - prevBottom - return if (deltaY < 0f) minOf(topDelta, bottomDelta) else maxOf(topDelta, bottomDelta) + state.topHeightOffset = (state.topHeightOffset + deltaY).coerceIn(-topLimit, 0f) + state.bottomHeightOffset = (state.bottomHeightOffset + deltaY).coerceIn(-bottomLimit, 0f) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt index 4620d750a..dd047f583 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt @@ -23,7 +23,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -52,6 +55,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R @@ -264,7 +268,7 @@ private fun DiscoverPages( }, accountViewModel = accountViewModel, ) { - HorizontalPager(state = pagerState, contentPadding = it) { page -> + HorizontalPager(state = pagerState) { page -> if (page >= 0 && page < feedTabs.size) { val tab = feedTabs[page] RefresheableBox(tab.feedState, true) { @@ -275,6 +279,7 @@ private fun DiscoverPages( routeForLastRead = tab.routeForLastRead, forceEventKind = tab.forceEventKind, listState = listState, + scaffoldPadding = it, accountViewModel = accountViewModel, nav = nav, ) @@ -286,6 +291,7 @@ private fun DiscoverPages( routeForLastRead = tab.routeForLastRead, forceEventKind = tab.forceEventKind, listState = listState, + scaffoldPadding = it, accountViewModel = accountViewModel, nav = nav, ) @@ -303,6 +309,7 @@ private fun RenderDiscoverFeed( routeForLastRead: String?, forceEventKind: Int?, listState: LazyGridState, + scaffoldPadding: PaddingValues, accountViewModel: AccountViewModel, nav: INav, ) { @@ -329,6 +336,7 @@ private fun RenderDiscoverFeed( routeForLastRead, listState, forceEventKind, + scaffoldPadding, accountViewModel, nav, ) @@ -391,6 +399,7 @@ private fun RenderDiscoverFeed( routeForLastRead: String?, forceEventKind: Int?, listState: LazyListState, + scaffoldPadding: PaddingValues, accountViewModel: AccountViewModel, nav: INav, ) { @@ -417,6 +426,7 @@ private fun RenderDiscoverFeed( routeForLastRead, listState, forceEventKind, + scaffoldPadding, accountViewModel, nav, ) @@ -460,13 +470,25 @@ private fun DiscoverFeedLoaded( routeForLastRead: String?, listState: LazyListState, forceEventKind: Int?, + scaffoldPadding: PaddingValues, accountViewModel: AccountViewModel, nav: INav, ) { val items by loaded.feed.collectAsStateWithLifecycle() + val layoutDirection = LocalLayoutDirection.current + val listPadding = + remember(scaffoldPadding, layoutDirection) { + PaddingValues( + start = scaffoldPadding.calculateStartPadding(layoutDirection), + top = scaffoldPadding.calculateTopPadding() + FeedPadding.calculateTopPadding(), + end = scaffoldPadding.calculateEndPadding(layoutDirection), + bottom = scaffoldPadding.calculateBottomPadding() + FeedPadding.calculateBottomPadding(), + ) + } + LazyColumn( - contentPadding = FeedPadding, + contentPadding = listPadding, state = listState, ) { itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item -> @@ -495,14 +517,26 @@ private fun DiscoverFeedColumnsLoaded( routeForLastRead: String?, listState: LazyGridState, forceEventKind: Int?, + scaffoldPadding: PaddingValues, accountViewModel: AccountViewModel, nav: INav, ) { val items by loaded.feed.collectAsStateWithLifecycle() + val layoutDirection = LocalLayoutDirection.current + val gridPadding = + remember(scaffoldPadding, layoutDirection) { + PaddingValues( + start = scaffoldPadding.calculateStartPadding(layoutDirection), + top = scaffoldPadding.calculateTopPadding() + FeedPadding.calculateTopPadding(), + end = scaffoldPadding.calculateEndPadding(layoutDirection), + bottom = scaffoldPadding.calculateBottomPadding() + FeedPadding.calculateBottomPadding(), + ) + } + LazyVerticalGrid( columns = GridCells.Fixed(2), - contentPadding = FeedPadding, + contentPadding = gridPadding, state = listState, ) { itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index 77adf8355..2e838b109 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -24,8 +24,11 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn @@ -51,7 +54,9 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R @@ -211,7 +216,6 @@ private fun HomePages( accountViewModel = accountViewModel, ) { HorizontalPager( - contentPadding = it, state = pagerState, userScrollEnabled = true, modifier = @@ -225,6 +229,7 @@ private fun HomePages( routeForLastRead = tabs[page].routeForLastRead, scrollStateKey = tabs[page].scrollStateKey, liveSection = tabs[page].liveSection, + scaffoldPadding = it, accountViewModel = accountViewModel, nav = nav, ) @@ -278,6 +283,7 @@ fun HomeFeeds( enablePullRefresh: Boolean = true, scrollStateKey: String? = null, liveSection: ChannelFeedContentState? = null, + scaffoldPadding: PaddingValues = PaddingValues(0.dp), accountViewModel: AccountViewModel, nav: INav, ) { @@ -289,7 +295,7 @@ fun HomeFeeds( listState = listState, nav = nav, routeForLastRead = routeForLastRead, - onLoaded = { FeedLoaded(it, listState, routeForLastRead, liveSection, accountViewModel, nav) }, + onLoaded = { FeedLoaded(it, listState, routeForLastRead, liveSection, scaffoldPadding, accountViewModel, nav) }, onEmpty = { HomeFeedEmpty(feedState::invalidateData) }, ) } @@ -303,13 +309,25 @@ fun FeedLoaded( listState: LazyListState, routeForLastRead: String?, liveSection: ChannelFeedContentState? = null, + scaffoldPadding: PaddingValues = PaddingValues(0.dp), accountViewModel: AccountViewModel, nav: INav, ) { val items by loaded.feed.collectAsStateWithLifecycle() + val layoutDirection = LocalLayoutDirection.current + val listPadding = + remember(scaffoldPadding, layoutDirection) { + PaddingValues( + start = scaffoldPadding.calculateStartPadding(layoutDirection), + top = scaffoldPadding.calculateTopPadding() + FeedPadding.calculateTopPadding(), + end = scaffoldPadding.calculateEndPadding(layoutDirection), + bottom = scaffoldPadding.calculateBottomPadding() + FeedPadding.calculateBottomPadding(), + ) + } + LazyColumn( - contentPadding = FeedPadding, + contentPadding = listPadding, state = listState, ) { if (liveSection != null) {