diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/RememberForeverStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/RememberForeverStates.kt index db08e6770..a164733ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/RememberForeverStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/RememberForeverStates.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.feeds -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.pager.PagerState @@ -115,20 +114,35 @@ fun rememberForeverLazyListState( return scrollState } -@OptIn(ExperimentalFoundationApi::class) @Composable fun rememberForeverPagerState( key: String, initialFirstVisibleItemIndex: Int = 0, initialFirstVisibleItemScrollOffset: Float = 0.0f, pageCount: () -> Int, +): PagerState = + rememberForeverPagerState(key, initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset, pageCount) { initialPage, initialPageOffsetFraction, pageCount -> + rememberPagerState(initialPage, initialPageOffsetFraction, pageCount) + } + +@Composable +fun rememberForeverPagerState( + key: String, + initialFirstVisibleItemIndex: Int = 0, + initialFirstVisibleItemScrollOffset: Float = 0.0f, + pageCount: () -> Int, + rememberPagerStateFunction: @Composable ( + initialPage: Int, + initialPageOffsetFraction: Float, + pageCount: () -> Int, + ) -> PagerState, ): PagerState { val savedValue = savedScrollStates[key] val savedIndex = savedValue?.index ?: initialFirstVisibleItemIndex val savedOffset = savedValue?.scrollOffsetFraction ?: initialFirstVisibleItemScrollOffset val scrollState = - rememberPagerState( + rememberPagerStateFunction( savedIndex, savedOffset, pageCount, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/PagerStateHack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/PagerStateHack.kt new file mode 100644 index 000000000..2c6b87cd9 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/PagerStateHack.kt @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.video + +import androidx.annotation.FloatRange +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.gestures.ScrollScope +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState +import kotlin.math.abs + +/** + * This file only exists to fix an interference between the Disappearing Top + * and Bottom Scaffold bars and the offsetFraction of the pager. The current + * implementation ends the scroll at a state where some fraction is still present + * which places videos away from their natural position in the page. + * + * The fix simply animates it back to the fraction = 0. + */ + +@Composable +fun myRememberPagerState( + initialPage: Int = 0, + @FloatRange(from = -0.5, to = 0.5) initialPageOffsetFraction: Float = 0f, + pageCount: () -> Int, +): PagerState = + rememberSaveable(saver = DefaultPagerState.Saver) { + DefaultPagerState( + initialPage, + initialPageOffsetFraction, + pageCount, + ) + }.apply { + pageCountState.value = pageCount + } + +@Composable +fun myRememberForeverPagerState( + key: String, + initialFirstVisibleItemIndex: Int = 0, + initialFirstVisibleItemScrollOffset: Float = 0.0f, + pageCount: () -> Int, +): PagerState = + rememberForeverPagerState(key, initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset, pageCount) { initialPage, initialPageOffsetFraction, pageCount -> + myRememberPagerState(initialPage, initialPageOffsetFraction, pageCount) + } + +private class DefaultPagerState( + currentPage: Int, + currentPageOffsetFraction: Float, + updatedPageCount: () -> Int, +) : PagerState(currentPage, currentPageOffsetFraction) { + var pageCountState = mutableStateOf(updatedPageCount) + override val pageCount: Int get() = pageCountState.value.invoke() + + override suspend fun scroll( + scrollPriority: MutatePriority, + block: suspend ScrollScope.() -> Unit, + ) { + super.scroll(scrollPriority, block) + if (abs(currentPageOffsetFraction) > 0) { + animateScrollToPage(currentPage, 0f) + } + } + + companion object { + /** + * To keep current page and current page offset saved + */ + val Saver: Saver = + listSaver( + save = { + listOf( + it.currentPage, + (it.currentPageOffsetFraction).coerceIn(-0.5f, 0.5f), + it.pageCount, + ) + }, + restore = { + DefaultPagerState( + currentPage = it[0] as Int, + currentPageOffsetFraction = it[1] as Float, + updatedPageCount = { it[2] as Int }, + ) + }, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt index ea2d0f342..c1b370ae8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video import androidx.compose.animation.core.tween -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -33,7 +32,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.VerticalPager -import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.Icon @@ -47,11 +45,11 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.FeatureSetType @@ -69,7 +67,6 @@ import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop -import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.Route @@ -236,7 +233,6 @@ private fun LoadedState( } } -@OptIn(ExperimentalFoundationApi::class) @Composable fun SlidingCarousel( loaded: FeedState.Loaded, @@ -249,9 +245,9 @@ fun SlidingCarousel( val pagerState = if (pagerStateKey != null) { - rememberForeverPagerState(pagerStateKey, items.list.size) { items.list.size } + myRememberForeverPagerState(pagerStateKey, items.list.size) { items.list.size } } else { - rememberPagerState(items.list.size) { items.list.size } + myRememberPagerState(items.list.size) { items.list.size } } WatchScrollToTop(videoFeedContentState, pagerState)