Tighten TV home hero and bring-in-view offset

This commit is contained in:
2026-04-19 20:07:08 +02:00
parent 76eebc1139
commit 52a51b1b28
4 changed files with 108 additions and 52 deletions

View File

@@ -43,7 +43,7 @@ fun TvHomeScreen(
) {
TvFocusedItemHero(
item = focusedMediaUiModel.value,
height = 250.dp
height = 220.dp
)
TvHomeContent(
libraries = libraries,

View File

@@ -6,7 +6,7 @@ import androidx.compose.foundation.gestures.BringIntoViewSpec
import androidx.compose.ui.unit.dp
import kotlin.math.abs
internal val TvHomeFocusedItemTopOffset = 56.dp
internal val TvHomeFocusedItemTopOffset = 42.dp
internal val TvHomeBringIntoViewTrailingSpace = 120.dp
private const val TvHomeRowPivotParentFraction = 0.3f

View File

@@ -3,6 +3,7 @@
package hu.bbara.purefin.ui.screen.home.components
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -11,9 +12,11 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import hu.bbara.purefin.core.ui.model.MediaUiModel
@@ -37,67 +40,71 @@ fun TvHomeContent(
modifier: Modifier = Modifier,
) {
val scheme = MaterialTheme.colorScheme
val topOffsetPx = with(LocalDensity.current) { TvHomeFocusedItemTopOffset.toPx() }
val hasContinueWatching = continueWatching.isNotEmpty()
val hasNextUp = nextUp.isNotEmpty()
val hasVisibleContent = hasContinueWatching || hasNextUp
val initialFocusRequester = remember { FocusRequester() }
LazyColumn(
modifier = modifier
.fillMaxSize()
.background(scheme.background)
.testTag(TvHomeContentViewportTag),
contentPadding = contentPadding
CompositionLocalProvider(
LocalBringIntoViewSpec provides tvHomeColumnBringIntoViewSpec(topOffsetPx)
) {
item(key = "tv-home-top-spacer") {
Spacer(modifier = Modifier.height(8.dp))
}
if (hasContinueWatching) {
item(key = "tv-home-continue-watching") {
TvContinueWatchingSection(
items = continueWatching,
LazyColumn(
modifier = modifier
.fillMaxSize()
.background(scheme.background)
.testTag(TvHomeContentViewportTag),
contentPadding = contentPadding
) {
item(key = "tv-home-top-spacer") {
Spacer(modifier = Modifier.height(8.dp))
}
if (hasContinueWatching) {
item(key = "tv-home-continue-watching") {
TvContinueWatchingSection(
items = continueWatching,
onFocusedItem = onMediaFocused,
onMovieSelected = onMovieSelected,
onEpisodeSelected = onEpisodeSelected,
firstItemFocusRequester = initialFocusRequester,
firstItemTestTag = TvHomeInitialFocusTag,
rowTestTag = TvHomeContinueWatchingRowTag
)
}
}
if (hasNextUp) {
item(key = "tv-home-next-up") {
TvNextUpSection(
items = nextUp,
onFocusedItem = onMediaFocused,
onEpisodeSelected = onEpisodeSelected,
firstItemFocusRequester = initialFocusRequester.takeIf { !hasContinueWatching },
firstItemTestTag = TvHomeInitialFocusTag.takeIf { !hasContinueWatching },
rowTestTag = TvHomeNextUpRowTag
)
}
}
itemsIndexed(
items = libraries,
key = { _, library -> library.id }
) { _, library ->
TvLibraryPosterSection(
title = library.name,
items = libraryContent[library.id].orEmpty(),
onFocusedItem = onMediaFocused,
onMovieSelected = onMovieSelected,
onEpisodeSelected = onEpisodeSelected,
firstItemFocusRequester = initialFocusRequester,
firstItemTestTag = TvHomeInitialFocusTag,
rowTestTag = TvHomeContinueWatchingRowTag
onMovieSelected = onMovieSelected,
onSeriesSelected = onSeriesSelected,
onEpisodeSelected = onEpisodeSelected
)
}
}
if (hasNextUp) {
item(key = "tv-home-next-up") {
TvNextUpSection(
items = nextUp,
onFocusedItem = onMediaFocused,
onEpisodeSelected = onEpisodeSelected,
firstItemFocusRequester = initialFocusRequester.takeIf { !hasContinueWatching },
firstItemTestTag = TvHomeInitialFocusTag.takeIf { !hasContinueWatching },
rowTestTag = TvHomeNextUpRowTag
)
}
}
itemsIndexed(
items = libraries,
key = { _, library -> library.id }
) { index, library ->
TvLibraryPosterSection(
title = library.name,
items = libraryContent[library.id].orEmpty(),
onFocusedItem = onMediaFocused,
firstItemFocusRequester = initialFocusRequester,
onMovieSelected = onMovieSelected,
onSeriesSelected = onSeriesSelected,
onEpisodeSelected = onEpisodeSelected
)
}
if (hasVisibleContent) {
item(key = "tv-home-trailing-space") {
Spacer(modifier = Modifier.height(TvHomeBringIntoViewTrailingSpace))
if (hasVisibleContent) {
item(key = "tv-home-trailing-space") {
Spacer(modifier = Modifier.height(TvHomeBringIntoViewTrailingSpace))
}
}
}
}

View File

@@ -0,0 +1,49 @@
package hu.bbara.purefin.ui.screen.home.components
import org.junit.Assert.assertEquals
import org.junit.Test
class TvHomeBringIntoViewSpecTest {
private val topOffsetPx = 56f
private val spec = tvHomeColumnBringIntoViewSpec(topOffsetPx)
@Test
fun returnsZero_whenFocusedItemAlreadyAtTopOffset() {
assertEquals(
0f,
spec.calculateScrollDistance(
offset = topOffsetPx,
size = 52f,
containerSize = 240f
),
0.0001f
)
}
@Test
fun scrollsDownToTopOffset_whenFocusedItemIsBelowTarget() {
assertEquals(
64f,
spec.calculateScrollDistance(
offset = 120f,
size = 52f,
containerSize = 240f
),
0.0001f
)
}
@Test
fun scrollsUpToTopOffset_whenFocusedItemIsAboveTarget() {
assertEquals(
-32f,
spec.calculateScrollDistance(
offset = 24f,
size = 52f,
containerSize = 240f
),
0.0001f
)
}
}