diff --git a/app-tv/src/androidTest/java/hu/bbara/purefin/common/ui/components/TvMediaDetailBringIntoViewSpecTest.kt b/app-tv/src/androidTest/java/hu/bbara/purefin/common/ui/components/TvMediaDetailBringIntoViewSpecTest.kt new file mode 100644 index 00000000..19d4954e --- /dev/null +++ b/app-tv/src/androidTest/java/hu/bbara/purefin/common/ui/components/TvMediaDetailBringIntoViewSpecTest.kt @@ -0,0 +1,49 @@ +package hu.bbara.purefin.common.ui.components + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TvMediaDetailBringIntoViewSpecTest { + + @Test + fun returnsZero_whenFocusedItemIsAlreadyFullyVisible() { + assertEquals( + 0f, + TvMediaDetailBringIntoViewSpec.calculateScrollDistance( + offset = 320f, + size = 52f, + containerSize = 1080f + ), + 0.0001f + ) + } + + @Test + fun scrollsOnlyEnoughToRevealFocusedItemBelowViewport() { + assertEquals( + 36f, + TvMediaDetailBringIntoViewSpec.calculateScrollDistance( + offset = 1064f, + size = 52f, + containerSize = 1080f + ), + 0.0001f + ) + } + + @Test + fun scrollsBackJustToTopEdge_whenFocusedItemMovesAboveViewport() { + assertEquals( + -24f, + TvMediaDetailBringIntoViewSpec.calculateScrollDistance( + offset = -24f, + size = 52f, + containerSize = 1080f + ), + 0.0001f + ) + } +} diff --git a/app-tv/src/main/java/hu/bbara/purefin/common/ui/components/MediaDetailShell.kt b/app-tv/src/main/java/hu/bbara/purefin/common/ui/components/MediaDetailShell.kt index 124df503..d4310688 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/common/ui/components/MediaDetailShell.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/common/ui/components/MediaDetailShell.kt @@ -1,6 +1,10 @@ +@file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class) + package hu.bbara.purefin.common.ui.components import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.BringIntoViewSpec +import androidx.compose.foundation.gestures.LocalBringIntoViewSpec import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope @@ -17,6 +21,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,6 +40,23 @@ private val MediaDetailHeaderTopPadding = 104.dp private val MediaDetailHeaderBottomPadding = 36.dp private const val MediaDetailBodyImageWidthFraction = 0.66f +internal val TvMediaDetailBringIntoViewSpec: BringIntoViewSpec = + object : BringIntoViewSpec { + override fun calculateScrollDistance( + offset: Float, + size: Float, + containerSize: Float, + ): Float { + val trailingEdge = offset + size + + return when { + offset < 0f -> offset + trailingEdge > containerSize -> trailingEdge - containerSize + else -> 0f + } + } + } + @Composable internal fun TvMediaDetailScaffold( resetScrollKey: Any, @@ -49,13 +71,15 @@ internal fun TvMediaDetailScaffold( listState.scrollToItem(0) } - LazyColumn( - state = listState, - modifier = modifier - .fillMaxSize() - .background(scheme.background) - ) { - bodyContent(contentPadding) + CompositionLocalProvider(LocalBringIntoViewSpec provides TvMediaDetailBringIntoViewSpec) { + LazyColumn( + state = listState, + modifier = modifier + .fillMaxSize() + .background(scheme.background) + ) { + bodyContent(contentPadding) + } } }