fix focus for android tv
This commit is contained in:
+1
@@ -39,6 +39,7 @@ fun AppBarWithSearchWithBackPreview() {
|
||||
@Composable
|
||||
fun InternalAppBarPreview() {
|
||||
SearchAppBar(
|
||||
Modifier,
|
||||
TextFieldValue(""),
|
||||
FocusRequester(),
|
||||
{},
|
||||
|
||||
+20
-2
@@ -18,7 +18,7 @@ import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -33,9 +33,12 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.withFrameNanos
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.FocusState
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusEvent
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -142,6 +145,7 @@ fun VideosInfo(
|
||||
emptyResource: StringResource,
|
||||
onClick: (item: VideoItem) -> Unit
|
||||
) {
|
||||
val requester = remember { FocusRequester() }
|
||||
val blur = LocalBlurState.current
|
||||
val gridState = rememberLazyGridState()
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
@@ -160,11 +164,19 @@ fun VideosInfo(
|
||||
),
|
||||
state = gridState
|
||||
) {
|
||||
items(items) { item ->
|
||||
itemsIndexed(
|
||||
items,
|
||||
key = { _, item -> item.id }
|
||||
) { index, item ->
|
||||
Poster(
|
||||
imageUrl = item.poster,
|
||||
label = item.title,
|
||||
onClick = { onClick(item) },
|
||||
modifier = if (index == 0) {
|
||||
Modifier.focusRequester(requester)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -172,6 +184,12 @@ fun VideosInfo(
|
||||
VideosNotFound(emptyResource)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(items) {
|
||||
if (items.isNotEmpty()) {
|
||||
withFrameNanos {}
|
||||
requester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+10
-1
@@ -35,6 +35,7 @@ import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -42,9 +43,12 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.withFrameNanos
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -240,6 +244,7 @@ fun DetailsInfo(
|
||||
paddingValues: PaddingValues,
|
||||
onWatchClicked: () -> Unit
|
||||
) {
|
||||
val requester = remember { FocusRequester() }
|
||||
val blur = LocalBlurState.current
|
||||
val showDetails = rememberSaveable { mutableStateOf(false) }
|
||||
LazyColumn(
|
||||
@@ -251,8 +256,12 @@ fun DetailsInfo(
|
||||
item {
|
||||
Button(
|
||||
onClick = onWatchClicked,
|
||||
modifier = Modifier.fillMaxWidth().padding(8.dp)
|
||||
modifier = Modifier.fillMaxWidth().padding(8.dp).focusRequester(requester)
|
||||
) { Text(stringResource(Res.string.watch)) }
|
||||
LaunchedEffect(null) {
|
||||
withFrameNanos {}
|
||||
requester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-13
@@ -30,14 +30,18 @@ import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.withFrameNanos
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
@@ -183,26 +187,39 @@ fun EpisodesOverviewList(
|
||||
ErrorAndRetry(errorMsg = errorCode) { loadLinks() }
|
||||
} else if (episodes != null) {
|
||||
val blur = LocalBlurState.current
|
||||
val requester = remember { FocusRequester() }
|
||||
ExpressiveLazyColumn(
|
||||
modifier = Modifier.applyBlurSource(blur).fillMaxSize(),
|
||||
contentPadding = contentPadding,
|
||||
content = {
|
||||
section {
|
||||
episodes.mapIndexed { index, episode ->
|
||||
item(key = episode.episode.episode) {
|
||||
EpisodeInfo(
|
||||
onLaunch,
|
||||
episode,
|
||||
onClipboardAction,
|
||||
index,
|
||||
onOfflineClicked,
|
||||
isSaveMode
|
||||
)
|
||||
}
|
||||
itemsIndexed(
|
||||
episodes,
|
||||
{ it.episode.episode }
|
||||
) { index, episode ->
|
||||
EpisodeInfo(
|
||||
if (index == 0) {
|
||||
Modifier.focusRequester(requester)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
onLaunch,
|
||||
episode,
|
||||
onClipboardAction,
|
||||
index,
|
||||
onOfflineClicked,
|
||||
isSaveMode
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
LaunchedEffect(episodes) {
|
||||
if (episodes.isNotEmpty()) {
|
||||
withFrameNanos {}
|
||||
requester.requestFocus()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Loading()
|
||||
}
|
||||
@@ -211,6 +228,7 @@ fun EpisodesOverviewList(
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun EpisodeInfo(
|
||||
modifier: Modifier,
|
||||
onLaunch: (Episode) -> Unit,
|
||||
ep: EpisodeWithOfflineInfo,
|
||||
onClipboardAction: (Episode) -> Unit,
|
||||
@@ -219,8 +237,7 @@ private fun EpisodeInfo(
|
||||
isSaveMode: Boolean
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
modifier = modifier.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
if (isSaveMode) {
|
||||
|
||||
+23
-4
@@ -14,7 +14,11 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.withFrameNanos
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import ru.shadowsparky.ui.LocalBlurState
|
||||
@@ -73,20 +77,35 @@ fun SeasonsInfo(
|
||||
onClick: (Long) -> Unit
|
||||
) {
|
||||
val blur = LocalBlurState.current
|
||||
val requester = remember { FocusRequester() }
|
||||
ExpressiveLazyColumn(
|
||||
Modifier.applyBlurSource(blur).fillMaxWidth(),
|
||||
contentPadding = paddingValues,
|
||||
content = {
|
||||
section {
|
||||
items(seasons) {
|
||||
SeasonInfo(it.title) { onClick(it.id) }
|
||||
itemsIndexed(seasons) { index, season ->
|
||||
SeasonInfo(
|
||||
if (index == 0) {
|
||||
Modifier.focusRequester(requester)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
season.title
|
||||
) { onClick(season.id) }
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(seasons) {
|
||||
if (seasons.isNotEmpty()) {
|
||||
withFrameNanos {}
|
||||
requester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SeasonInfo(titleText: String, onClick: () -> Unit) {
|
||||
Text(titleText, modifier = Modifier.fillMaxWidth().clickable { onClick() }.padding(16.dp))
|
||||
fun SeasonInfo(modifier: Modifier, titleText: String, onClick: () -> Unit) {
|
||||
Text(titleText, modifier = modifier.fillMaxWidth().clickable { onClick() }.padding(16.dp))
|
||||
}
|
||||
|
||||
+3
-1
@@ -92,6 +92,7 @@ fun SearchScreen(
|
||||
BaseMainScreen(
|
||||
appBar = {
|
||||
SearchAppBar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textFieldValue = textFieldValue,
|
||||
onDone = { component.done(textFieldValue.text) },
|
||||
onValueChange = { textFieldValue = it },
|
||||
@@ -213,6 +214,7 @@ expect fun Modifier.requestFocusOnDownEvent(focusRequester: FocusRequester): Mod
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun SearchAppBar(
|
||||
modifier: Modifier,
|
||||
textFieldValue: TextFieldValue,
|
||||
focusRequester: FocusRequester,
|
||||
onDone: () -> Unit,
|
||||
@@ -228,7 +230,7 @@ fun SearchAppBar(
|
||||
}
|
||||
val blur = LocalBlurState.current
|
||||
AppBarWithSearch(
|
||||
modifier = Modifier.applyBlurEffect(blur),
|
||||
modifier = modifier.applyBlurEffect(blur),
|
||||
state = rememberSearchBarState(),
|
||||
colors = ComponentDefaults.appBarWithSearchDefaults(),
|
||||
inputField = {
|
||||
|
||||
+12
-1
@@ -12,9 +12,14 @@ import androiddev.apps.vbox.client.shared.generated.resources.user_tags
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.withFrameNanos
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import ru.shadowsparky.ui.LocalBlurState
|
||||
import ru.shadowsparky.ui.applyBlurSource
|
||||
@@ -28,6 +33,7 @@ fun SettingsContent(comp: SettingsComponent, paddingValues: PaddingValues) {
|
||||
val tokenInfo by comp.tokenInfo.collectAsState(null)
|
||||
val externalPlayerState by comp.externalPlayerState.collectAsState(null)
|
||||
val blur = LocalBlurState.current
|
||||
val requester = remember { FocusRequester() }
|
||||
ExpressiveLazyColumn(
|
||||
modifier = Modifier.applyBlurSource(blur),
|
||||
contentPadding = paddingValues,
|
||||
@@ -47,7 +53,8 @@ fun SettingsContent(comp: SettingsComponent, paddingValues: PaddingValues) {
|
||||
SwitchPreference(
|
||||
text = stringResource(Res.string.external_player_title),
|
||||
state = it,
|
||||
onStateChanged = { newState -> comp.setExternalPlayerState(newState) }
|
||||
onStateChanged = { newState -> comp.setExternalPlayerState(newState) },
|
||||
modifier = Modifier.focusRequester(requester)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -99,4 +106,8 @@ fun SettingsContent(comp: SettingsComponent, paddingValues: PaddingValues) {
|
||||
}
|
||||
}
|
||||
)
|
||||
LaunchedEffect(null) {
|
||||
withFrameNanos {}
|
||||
requester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,20 @@ class ExpressiveSectionScope {
|
||||
contentType: ((T) -> Any)? = null,
|
||||
itemContent: @Composable (T) -> Unit
|
||||
) {
|
||||
items.forEach { t ->
|
||||
itemsIndexed(items, key, contentType) { _, t -> itemContent(t) }
|
||||
}
|
||||
|
||||
fun <T> itemsIndexed(
|
||||
items: List<T>,
|
||||
key: ((T) -> Any)? = null,
|
||||
contentType: ((T) -> Any)? = null,
|
||||
itemContent: @Composable (Int, T) -> Unit
|
||||
) {
|
||||
items.forEachIndexed { index, t ->
|
||||
nodes += ExpressiveNode(
|
||||
key = key?.invoke(t),
|
||||
contentType = contentType?.invoke(t),
|
||||
content = { itemContent(t) }
|
||||
content = { itemContent(index, t) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+14
-44
@@ -42,11 +42,10 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.withFrameNanos
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusProperties
|
||||
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.Color
|
||||
@@ -144,10 +143,7 @@ fun DefaultControls(
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.focusableWithBorder(focus.slider) {
|
||||
next = if (audioTracksState.isNotEmpty()) focus.audioTrack else focus.lock
|
||||
previous = focus.next
|
||||
}
|
||||
.focusableWithBorder(focus.slider)
|
||||
.onPreviewKeyEvent {
|
||||
if (it.type == KeyEventType.KeyDown) {
|
||||
when (it.key) {
|
||||
@@ -179,10 +175,7 @@ private fun LockUiButton(
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { onChanged(!state) },
|
||||
modifier = Modifier.focusableWithBorder(focusContainer.lock) {
|
||||
next = focusContainer.prev
|
||||
previous = if (hasAudioTrack) focusContainer.audioTrack else focusContainer.slider
|
||||
}
|
||||
modifier = Modifier.focusableWithBorder(focusContainer.lock)
|
||||
) {
|
||||
val icon = if (state) Res.drawable.lock_24px else Res.drawable.lock_open_24px
|
||||
Icon(painterResource(icon), null, tint = Color.White)
|
||||
@@ -202,10 +195,7 @@ private fun AudioTracksButton(
|
||||
val tracks = tracks.map { track -> track to FocusRequester() }
|
||||
IconButton(
|
||||
onClick = { expanded = true },
|
||||
modifier = Modifier.focusableWithBorder(focusContainer.audioTrack) {
|
||||
next = focusContainer.lock
|
||||
previous = focusContainer.slider
|
||||
}
|
||||
modifier = Modifier.focusableWithBorder(focusContainer.audioTrack)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(Res.drawable.music_note_24px),
|
||||
@@ -239,10 +229,7 @@ private fun AudioTracksButton(
|
||||
expanded = false
|
||||
onSelect(track.id)
|
||||
},
|
||||
modifier = Modifier.focusableWithBorder(requester) {
|
||||
previous = tracks.getOrNull(i - 1)?.second ?: focusContainer.audioTrack
|
||||
next = tracks.getOrNull(i + 1)?.second ?: focusContainer.audioTrack
|
||||
}
|
||||
modifier = Modifier.focusableWithBorder(requester)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -311,10 +298,7 @@ private fun BoxScope.CenterControls(
|
||||
controller.seekTo(0)
|
||||
controller.play()
|
||||
}
|
||||
}, modifier = Modifier.focusableWithBorder(focusContainer.next) {
|
||||
previous = focusContainer.lock
|
||||
next = focusContainer.slider
|
||||
}
|
||||
}, modifier = Modifier.focusableWithBorder(focusContainer.next)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(Res.drawable.replay_24px),
|
||||
@@ -333,10 +317,7 @@ private fun BoxScope.CenterControls(
|
||||
IconButton(
|
||||
onClick = {
|
||||
scope.launch { controller.seek(true) }
|
||||
}, modifier = Modifier.focusableWithBorder(focusContainer.prev) {
|
||||
next = focusContainer.next
|
||||
previous = focusContainer.play
|
||||
}
|
||||
}, modifier = Modifier.focusableWithBorder(focusContainer.prev)
|
||||
) {
|
||||
Text(
|
||||
text = "-$DEF_SEEK_TIME",
|
||||
@@ -350,10 +331,7 @@ private fun BoxScope.CenterControls(
|
||||
scope.launch {
|
||||
if (state.isPlaying) controller.pause() else controller.play()
|
||||
}
|
||||
}, modifier = Modifier.focusableWithBorder(focusContainer.next) {
|
||||
next = focusContainer.next
|
||||
previous = focusContainer.prev
|
||||
}
|
||||
}, modifier = Modifier.focusableWithBorder(focusContainer.play)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
@@ -371,10 +349,7 @@ private fun BoxScope.CenterControls(
|
||||
IconButton(
|
||||
onClick = {
|
||||
scope.launch { controller.seek(false) }
|
||||
}, modifier = Modifier.focusableWithBorder(focusContainer.next) {
|
||||
next = focusContainer.slider
|
||||
previous = focusContainer.play
|
||||
}
|
||||
}, modifier = Modifier.focusableWithBorder(focusContainer.next)
|
||||
) {
|
||||
Text(
|
||||
text = "+$DEF_SEEK_TIME",
|
||||
@@ -383,9 +358,8 @@ private fun BoxScope.CenterControls(
|
||||
)
|
||||
}
|
||||
LaunchedEffect(null) {
|
||||
delay(1_000)
|
||||
val result = focusContainer.play.requestFocus()
|
||||
println("DefaultControls focus changed? $result")
|
||||
withFrameNanos {}
|
||||
focusContainer.play.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,18 +408,14 @@ private class FocusContainer(
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun Modifier.focusableWithBorder(
|
||||
requester: FocusRequester,
|
||||
focusProperties: FocusProperties.() -> Unit
|
||||
): Modifier {
|
||||
private fun Modifier.focusableWithBorder(requester: FocusRequester): Modifier {
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
return focusRequester(requester)
|
||||
.focusProperties(focusProperties)
|
||||
return this.focusRequester(requester)
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.border(
|
||||
width = 2.dp,
|
||||
color = if (isFocused) {
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
Color.Transparent
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user