mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Fixes focusing issues on many rows (#718)
## Description Fixes focus jumping back a few cards on rows when returning to a page I think some behavior changes occurred in a recent compose release. `requestFocus` doesn't throw an exception anymore for example. Focus in compose has always been finicky. This PR basically adds some delegating parent focus down its to children. Also, most rows now maintain an internal position state to track where the focus requester is applied. ### Related issues Closes #632
This commit is contained in:
parent
154713d3f1
commit
08355bf24b
6 changed files with 74 additions and 60 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.ui.cards
|
package com.github.damontecres.wholphin.ui.cards
|
||||||
|
|
||||||
|
import androidx.compose.foundation.focusGroup
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
|
@ -8,15 +9,20 @@ import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.focusRestorer
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.ui.rememberInt
|
||||||
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> ItemRow(
|
fun <T> ItemRow(
|
||||||
|
|
@ -36,9 +42,16 @@ fun <T> ItemRow(
|
||||||
) {
|
) {
|
||||||
val state = rememberLazyListState()
|
val state = rememberLazyListState()
|
||||||
val firstFocus = remember { FocusRequester() }
|
val firstFocus = remember { FocusRequester() }
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
var position by rememberInt()
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = modifier,
|
modifier =
|
||||||
|
modifier.focusProperties {
|
||||||
|
onEnter = {
|
||||||
|
focusRequester.tryRequestFocus()
|
||||||
|
}
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
|
|
@ -52,11 +65,13 @@ fun <T> ItemRow(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.focusRestorer(firstFocus),
|
.focusGroup()
|
||||||
|
.focusRestorer(firstFocus)
|
||||||
|
.focusRequester(focusRequester),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(items) { index, item ->
|
itemsIndexed(items) { index, item ->
|
||||||
val cardModifier =
|
val cardModifier =
|
||||||
if (index == 0) {
|
if (index == position) {
|
||||||
Modifier.focusRequester(firstFocus)
|
Modifier.focusRequester(firstFocus)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -65,8 +80,14 @@ fun <T> ItemRow(
|
||||||
index,
|
index,
|
||||||
item,
|
item,
|
||||||
cardModifier,
|
cardModifier,
|
||||||
{ if (item != null) onClickItem.invoke(index, item) },
|
{
|
||||||
{ if (item != null) onLongClickItem.invoke(index, item) },
|
position = index
|
||||||
|
if (item != null) onClickItem.invoke(index, item)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position = index
|
||||||
|
if (item != null) onLongClickItem.invoke(index, item)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
|
@ -24,6 +26,7 @@ import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
||||||
import com.github.damontecres.wholphin.data.model.Person
|
import com.github.damontecres.wholphin.data.model.Person
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
|
import com.github.damontecres.wholphin.ui.rememberInt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PersonRow(
|
fun PersonRow(
|
||||||
|
|
@ -34,6 +37,7 @@ fun PersonRow(
|
||||||
onLongClick: ((Int, Person) -> Unit)? = null,
|
onLongClick: ((Int, Person) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val firstFocus = remember { FocusRequester() }
|
val firstFocus = remember { FocusRequester() }
|
||||||
|
var position by rememberInt()
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -56,12 +60,18 @@ fun PersonRow(
|
||||||
itemsIndexed(people) { index, person ->
|
itemsIndexed(people) { index, person ->
|
||||||
PersonCard(
|
PersonCard(
|
||||||
person = person,
|
person = person,
|
||||||
onClick = { onClick.invoke(person) },
|
onClick = {
|
||||||
onLongClick = { onLongClick?.invoke(index, person) },
|
position = index
|
||||||
|
onClick.invoke(person)
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
position = index
|
||||||
|
onLongClick?.invoke(index, person)
|
||||||
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.width(personRowCardWidth)
|
.width(personRowCardWidth)
|
||||||
.ifElse(index == 0, Modifier.focusRequester(firstFocus))
|
.ifElse(index == position, Modifier.focusRequester(firstFocus))
|
||||||
.animateItem(),
|
.animateItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ import com.github.damontecres.wholphin.ui.data.VideoSortOptions
|
||||||
import com.github.damontecres.wholphin.ui.logTab
|
import com.github.damontecres.wholphin.ui.logTab
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -72,7 +71,6 @@ fun CollectionFolderMovie(
|
||||||
|
|
||||||
var showHeader by rememberSaveable { mutableStateOf(true) }
|
var showHeader by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,6 @@ fun CollectionFolderTv(
|
||||||
|
|
||||||
var showHeader by rememberSaveable { mutableStateOf(true) }
|
var showHeader by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ import com.github.damontecres.wholphin.ui.formatDateTime
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
import com.github.damontecres.wholphin.ui.logTab
|
import com.github.damontecres.wholphin.ui.logTab
|
||||||
import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp
|
import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp
|
||||||
|
import com.github.damontecres.wholphin.ui.rememberInt
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.ui.util.rememberDelayedNestedScroll
|
import com.github.damontecres.wholphin.ui.util.rememberDelayedNestedScroll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -196,7 +197,7 @@ fun SeriesOverviewContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val state = rememberLazyListState(position.episodeRowIndex)
|
val state = rememberLazyListState(position.episodeRowIndex)
|
||||||
|
var epPosition by rememberInt()
|
||||||
LazyRow(
|
LazyRow(
|
||||||
state = state,
|
state = state,
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
|
@ -204,7 +205,7 @@ fun SeriesOverviewContent(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.focusRestorer(firstItemFocusRequester)
|
.focusRestorer(firstItemFocusRequester)
|
||||||
.focusRequester(episodeRowFocusRequester)
|
// .focusRequester(episodeRowFocusRequester)
|
||||||
.onFocusChanged {
|
.onFocusChanged {
|
||||||
cardRowHasFocus = it.hasFocus
|
cardRowHasFocus = it.hasFocus
|
||||||
},
|
},
|
||||||
|
|
@ -230,19 +231,23 @@ fun SeriesOverviewContent(
|
||||||
playPercent =
|
playPercent =
|
||||||
episode?.data?.userData?.playedPercentage
|
episode?.data?.userData?.playedPercentage
|
||||||
?: 0.0,
|
?: 0.0,
|
||||||
onClick = { if (episode != null) onClick.invoke(episode) },
|
onClick = {
|
||||||
|
epPosition = episodeIndex
|
||||||
|
if (episode != null) onClick.invoke(episode)
|
||||||
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
if (episode != null) {
|
epPosition = episodeIndex
|
||||||
onLongClick.invoke(
|
if (episode != null) onLongClick.invoke(episode)
|
||||||
episode,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.ifElse(
|
.ifElse(
|
||||||
episodeIndex == position.episodeRowIndex,
|
episodeIndex == position.episodeRowIndex,
|
||||||
Modifier.focusRequester(firstItemFocusRequester),
|
Modifier
|
||||||
|
.focusRequester(firstItemFocusRequester),
|
||||||
|
).ifElse(
|
||||||
|
episodeIndex == epPosition,
|
||||||
|
Modifier.focusRequester(episodeRowFocusRequester),
|
||||||
).ifElse(
|
).ifElse(
|
||||||
episodeIndex != position.episodeRowIndex,
|
episodeIndex != position.episodeRowIndex,
|
||||||
Modifier
|
Modifier
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.ui.main
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.focusGroup
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
|
@ -24,7 +25,6 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|
@ -61,7 +61,6 @@ import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.components.QuickDetails
|
import com.github.damontecres.wholphin.ui.components.QuickDetails
|
||||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumnSaver
|
|
||||||
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||||
|
|
@ -70,6 +69,7 @@ import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp
|
import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp
|
||||||
import com.github.damontecres.wholphin.ui.playback.playable
|
import com.github.damontecres.wholphin.ui.playback.playable
|
||||||
|
import com.github.damontecres.wholphin.ui.rememberPosition
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
|
@ -209,52 +209,32 @@ fun HomePageContent(
|
||||||
onFocusPosition: ((RowColumn) -> Unit)? = null,
|
onFocusPosition: ((RowColumn) -> Unit)? = null,
|
||||||
loadingState: LoadingState? = null,
|
loadingState: LoadingState? = null,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
var position by rememberPosition()
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val firstRow =
|
|
||||||
remember {
|
|
||||||
homeRows
|
|
||||||
.indexOfFirst {
|
|
||||||
when (it) {
|
|
||||||
is HomeRowLoadingState.Error -> false
|
|
||||||
is HomeRowLoadingState.Loading -> true
|
|
||||||
is HomeRowLoadingState.Pending -> true
|
|
||||||
is HomeRowLoadingState.Success -> it.items.isNotEmpty()
|
|
||||||
}
|
|
||||||
}.coerceAtLeast(0)
|
|
||||||
}
|
|
||||||
var position by rememberSaveable(stateSaver = RowColumnSaver) {
|
|
||||||
mutableStateOf(RowColumn(firstRow, 0))
|
|
||||||
}
|
|
||||||
val focusedItem =
|
val focusedItem =
|
||||||
position.let {
|
position.let {
|
||||||
(homeRows.getOrNull(it.row) as? HomeRowLoadingState.Success)?.items?.getOrNull(it.column)
|
(homeRows.getOrNull(it.row) as? HomeRowLoadingState.Success)?.items?.getOrNull(it.column)
|
||||||
}
|
}
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val rowFocusRequesters = remember(homeRows.size) { List(homeRows.size) { FocusRequester() } }
|
val rowFocusRequesters = remember(homeRows) { List(homeRows.size) { FocusRequester() } }
|
||||||
var firstFocused by rememberSaveable { mutableStateOf(false) }
|
var firstFocused by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(homeRows) {
|
LaunchedEffect(homeRows) {
|
||||||
if (!firstFocused) {
|
if (!firstFocused) {
|
||||||
// Waiting for the first home row to load, then focus on it
|
if (position.row >= 0) {
|
||||||
homeRows
|
rowFocusRequesters[position.row].tryRequestFocus()
|
||||||
.indexOfFirst { it is HomeRowLoadingState.Success && it.items.isNotEmpty() }
|
firstFocused = true
|
||||||
.takeIf { it >= 0 }
|
} else {
|
||||||
?.let {
|
// Waiting for the first home row to load, then focus on it
|
||||||
rowFocusRequesters[it].tryRequestFocus()
|
homeRows
|
||||||
delay(50)
|
.indexOfFirst { it is HomeRowLoadingState.Success && it.items.isNotEmpty() }
|
||||||
listState.scrollToItem(it)
|
.takeIf { it >= 0 }
|
||||||
firstFocused = true
|
?.let {
|
||||||
}
|
rowFocusRequesters[it].tryRequestFocus()
|
||||||
}
|
firstFocused = true
|
||||||
}
|
delay(50)
|
||||||
LaunchedEffect(Unit) {
|
listState.scrollToItem(it)
|
||||||
if (firstFocused) {
|
}
|
||||||
// After the first home row was loaded & focused, page recompositions should focus on the positioned row
|
}
|
||||||
val index = position.row
|
|
||||||
rowFocusRequesters.getOrNull(index)?.tryRequestFocus()
|
|
||||||
delay(50)
|
|
||||||
listState.scrollToItem(index)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LaunchedEffect(position) {
|
LaunchedEffect(position) {
|
||||||
|
|
@ -353,6 +333,7 @@ fun HomePageContent(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.focusGroup()
|
||||||
.focusRequester(rowFocusRequesters[rowIndex])
|
.focusRequester(rowFocusRequesters[rowIndex])
|
||||||
.animateItem(),
|
.animateItem(),
|
||||||
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue