Fix TV home row focus navigation

This commit is contained in:
2026-03-28 22:40:25 +01:00
parent fb4b8a56fa
commit a187192013
3 changed files with 73 additions and 33 deletions

View File

@@ -24,6 +24,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
@@ -93,7 +94,11 @@ fun PosterCard(
Column(
modifier = modifier
.width(posterWidth)
.graphicsLayer { scaleX = scale; scaleY = scale }
.graphicsLayer {
scaleX = scale
scaleY = scale
transformOrigin = TransformOrigin(0.5f, 0f)
}
) {
Box() {
PurefinAsyncImage(

View File

@@ -33,15 +33,31 @@ fun TvHomeContent(
val visibleLibraries = remember(libraries, libraryContent) {
libraries.filter { libraryContent[it.id]?.isEmpty() != true }
}
val continueWatchingFocusRequester = remember { FocusRequester() }
val nextUpFocusRequester = remember { FocusRequester() }
val libraryFocusRequesters = remember(visibleLibraries.map { it.id }) {
visibleLibraries.associate { it.id to FocusRequester() }
val continueWatchingSectionFocusRequester = remember { FocusRequester() }
val continueWatchingFirstItemFocusRequester = remember { FocusRequester() }
val nextUpSectionFocusRequester = remember { FocusRequester() }
val nextUpFirstItemFocusRequester = remember { FocusRequester() }
val librarySectionFocusRequesters = remember(visibleLibraries.map { it.id }) {
visibleLibraries.associate { library -> library.id to FocusRequester() }
}
val libraryFirstItemFocusRequesters = remember(visibleLibraries.map { it.id }) {
visibleLibraries.associate { library -> library.id to FocusRequester() }
}
val firstSectionFocusRequester = when {
continueWatching.isNotEmpty() -> continueWatchingFocusRequester
nextUp.isNotEmpty() -> nextUpFocusRequester
else -> visibleLibraries.firstOrNull()?.let { libraryFocusRequesters[it.id] }
continueWatching.isNotEmpty() -> continueWatchingSectionFocusRequester
nextUp.isNotEmpty() -> nextUpSectionFocusRequester
else -> visibleLibraries.firstOrNull()?.let { librarySectionFocusRequesters[it.id] }
}
val firstVisibleLibrarySectionFocusRequester = visibleLibraries.firstOrNull()
?.let { librarySectionFocusRequesters[it.id] }
val firstHomeRowBelowContinueWatching = when {
nextUp.isNotEmpty() -> nextUpSectionFocusRequester
else -> firstVisibleLibrarySectionFocusRequester
}
val firstHomeRowAboveLibraries = when {
nextUp.isNotEmpty() -> nextUpSectionFocusRequester
continueWatching.isNotEmpty() -> continueWatchingSectionFocusRequester
else -> null
}
LaunchedEffect(firstSectionFocusRequester) {
@@ -59,11 +75,9 @@ fun TvHomeContent(
item {
TvContinueWatchingSection(
items = continueWatching,
firstItemFocusRequester = continueWatchingFocusRequester,
downFocusRequester = when {
nextUp.isNotEmpty() -> nextUpFocusRequester
else -> visibleLibraries.firstOrNull()?.let { libraryFocusRequesters[it.id] }
},
sectionFocusRequester = continueWatchingSectionFocusRequester,
firstItemFocusRequester = continueWatchingFirstItemFocusRequester,
downFocusRequester = firstHomeRowBelowContinueWatching,
onMovieSelected = onMovieSelected,
onEpisodeSelected = onEpisodeSelected
)
@@ -74,9 +88,11 @@ fun TvHomeContent(
item {
TvNextUpSection(
items = nextUp,
firstItemFocusRequester = nextUpFocusRequester,
upFocusRequester = continueWatching.takeIf { it.isNotEmpty() }?.let { continueWatchingFocusRequester },
downFocusRequester = visibleLibraries.firstOrNull()?.let { libraryFocusRequesters[it.id] },
sectionFocusRequester = nextUpSectionFocusRequester,
firstItemFocusRequester = nextUpFirstItemFocusRequester,
upFocusRequester = continueWatching.takeIf { it.isNotEmpty() }
?.let { continueWatchingSectionFocusRequester },
downFocusRequester = firstVisibleLibrarySectionFocusRequester,
onEpisodeSelected = onEpisodeSelected
)
}
@@ -88,28 +104,25 @@ fun TvHomeContent(
key = { it.id }
) { item ->
val libraryIndex = visibleLibraries.indexOfFirst { it.id == item.id }
val previousLibraryFocusRequester = visibleLibraries
val previousLibrarySectionFocusRequester = visibleLibraries
.getOrNull(libraryIndex - 1)
?.let { libraryFocusRequesters[it.id] }
val nextLibraryFocusRequester = visibleLibraries
?.let { librarySectionFocusRequesters[it.id] }
val nextLibrarySectionFocusRequester = visibleLibraries
.getOrNull(libraryIndex + 1)
?.let { libraryFocusRequesters[it.id] }
?.let { librarySectionFocusRequesters[it.id] }
TvLibraryPosterSection(
title = item.name,
items = libraryContent[item.id] ?: emptyList(),
action = "See All",
firstItemFocusRequester = libraryFocusRequesters[item.id],
sectionFocusRequester = librarySectionFocusRequesters.getValue(item.id),
firstItemFocusRequester = libraryFirstItemFocusRequesters.getValue(item.id),
upFocusRequester = if (libraryIndex == 0) {
when {
nextUp.isNotEmpty() -> nextUpFocusRequester
continueWatching.isNotEmpty() -> continueWatchingFocusRequester
else -> null
}
firstHomeRowAboveLibraries
} else {
previousLibraryFocusRequester
previousLibrarySectionFocusRequester
},
downFocusRequester = nextLibraryFocusRequester,
downFocusRequester = nextLibrarySectionFocusRequester,
onMovieSelected = onMovieSelected,
onSeriesSelected = onSeriesSelected,
onEpisodeSelected = onEpisodeSelected

View File

@@ -32,6 +32,8 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
@@ -54,6 +56,7 @@ import kotlin.math.nextUp
@Composable
fun TvContinueWatchingSection(
items: List<ContinueWatchingItem>,
sectionFocusRequester: FocusRequester,
firstItemFocusRequester: FocusRequester? = null,
upFocusRequester: FocusRequester? = null,
downFocusRequester: FocusRequester? = null,
@@ -67,7 +70,10 @@ fun TvContinueWatchingSection(
action = null
)
LazyRow(
modifier = modifier.fillMaxWidth(),
modifier = modifier
.fillMaxWidth()
.focusRequester(sectionFocusRequester)
.focusRestorer(firstItemFocusRequester ?: FocusRequester.Default),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
@@ -161,7 +167,11 @@ fun TvContinueWatchingCard(
modifier = modifier
.width(cardWidth)
.wrapContentHeight()
.graphicsLayer { scaleX = scale; scaleY = scale }
.graphicsLayer {
scaleX = scale
scaleY = scale
transformOrigin = TransformOrigin(0.5f, 0f)
}
) {
Box(
modifier = Modifier
@@ -217,6 +227,7 @@ fun TvContinueWatchingCard(
@Composable
fun TvNextUpSection(
items: List<NextUpItem>,
sectionFocusRequester: FocusRequester,
firstItemFocusRequester: FocusRequester? = null,
upFocusRequester: FocusRequester? = null,
downFocusRequester: FocusRequester? = null,
@@ -229,7 +240,10 @@ fun TvNextUpSection(
action = null
)
LazyRow(
modifier = modifier.fillMaxWidth(),
modifier = modifier
.fillMaxWidth()
.focusRequester(sectionFocusRequester)
.focusRestorer(firstItemFocusRequester ?: FocusRequester.Default),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
@@ -313,7 +327,11 @@ fun TvNextUpCard(
modifier = modifier
.width(cardWidth)
.wrapContentHeight()
.graphicsLayer { scaleX = scale; scaleY = scale }
.graphicsLayer {
scaleX = scale
scaleY = scale
transformOrigin = TransformOrigin(0.5f, 0f)
}
) {
Box(
modifier = Modifier
@@ -364,6 +382,7 @@ fun TvLibraryPosterSection(
title: String,
items: List<PosterItem>,
action: String?,
sectionFocusRequester: FocusRequester,
firstItemFocusRequester: FocusRequester? = null,
upFocusRequester: FocusRequester? = null,
downFocusRequester: FocusRequester? = null,
@@ -377,7 +396,10 @@ fun TvLibraryPosterSection(
action = action
)
LazyRow(
modifier = modifier.fillMaxWidth(),
modifier = modifier
.fillMaxWidth()
.focusRequester(sectionFocusRequester)
.focusRestorer(firstItemFocusRequester ?: FocusRequester.Default),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {