rewrite focus logic in player screen
This commit is contained in:
+1
-1
@@ -171,7 +171,7 @@ private fun Poster(
|
|||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.aspectRatio(0.7f)
|
.aspectRatio(0.7f)
|
||||||
.focusBorder()
|
.focusBorder(true)
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
|
|||||||
+2
-1
@@ -31,6 +31,7 @@ import org.jetbrains.compose.resources.stringResource
|
|||||||
import ru.shadowsparky.ui.AppScaffold
|
import ru.shadowsparky.ui.AppScaffold
|
||||||
import ru.shadowsparky.ui.components.ErrorContent
|
import ru.shadowsparky.ui.components.ErrorContent
|
||||||
import ru.shadowsparky.ui.components.Loading
|
import ru.shadowsparky.ui.components.Loading
|
||||||
|
import ru.shadowsparky.ui.theme.focusBorder
|
||||||
import ru.shadowsparky.videoplayer.PlayerEvent
|
import ru.shadowsparky.videoplayer.PlayerEvent
|
||||||
import ru.shadowsparky.videoplayer.ui.VideoPlayer
|
import ru.shadowsparky.videoplayer.ui.VideoPlayer
|
||||||
|
|
||||||
@@ -107,7 +108,7 @@ private fun PlayerActionOverlay(
|
|||||||
color = MaterialTheme.colorScheme.primaryContainer,
|
color = MaterialTheme.colorScheme.primaryContainer,
|
||||||
tonalElevation = 0.dp,
|
tonalElevation = 0.dp,
|
||||||
shadowElevation = 0.dp,
|
shadowElevation = 0.dp,
|
||||||
modifier = Modifier.padding(end = 20.dp).alpha(0.6f)
|
modifier = Modifier.padding(end = 20.dp).alpha(0.6f).focusBorder()
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
|
|||||||
+2
-2
@@ -280,7 +280,7 @@ private fun MessageBubble(
|
|||||||
var boxModifier = Modifier.widthIn(max = 280.dp)
|
var boxModifier = Modifier.widthIn(max = 280.dp)
|
||||||
if (isTv) {
|
if (isTv) {
|
||||||
boxModifier = boxModifier.focusRequester(itemFocusRequester)
|
boxModifier = boxModifier.focusRequester(itemFocusRequester)
|
||||||
.focusBorder(false)
|
.focusBorder()
|
||||||
.onKeyEvent {
|
.onKeyEvent {
|
||||||
if (it.type == KeyEventType.KeyDown) {
|
if (it.type == KeyEventType.KeyDown) {
|
||||||
when (it.key) {
|
when (it.key) {
|
||||||
@@ -356,7 +356,7 @@ private fun TextDialog(
|
|||||||
} else {
|
} else {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
itemsIndexed(links) { index, item ->
|
itemsIndexed(links) { index, item ->
|
||||||
val mod = Modifier.focusBorder(false)
|
val mod = Modifier.focusBorder()
|
||||||
TextPreference(
|
TextPreference(
|
||||||
text = item.text,
|
text = item.text,
|
||||||
onClick = { onClickLink(item.text) },
|
onClick = { onClickLink(item.text) },
|
||||||
|
|||||||
+26
-81
@@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.CircularWavyProgressIndicator
|
import androidx.compose.material3.CircularWavyProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
@@ -49,14 +48,8 @@ import androidx.compose.ui.focus.FocusRequester
|
|||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.key.Key
|
|
||||||
import androidx.compose.ui.input.key.KeyEventType
|
|
||||||
import androidx.compose.ui.input.key.key
|
|
||||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
|
||||||
import androidx.compose.ui.input.key.type
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import ru.shadowsparky.ui.components.BackButton
|
import ru.shadowsparky.ui.components.BackButton
|
||||||
@@ -67,7 +60,6 @@ import ru.shadowsparky.videoplayer.AudioTrackInfo
|
|||||||
import ru.shadowsparky.videoplayer.ControlsState
|
import ru.shadowsparky.videoplayer.ControlsState
|
||||||
import ru.shadowsparky.videoplayer.PlayerController
|
import ru.shadowsparky.videoplayer.PlayerController
|
||||||
import ru.shadowsparky.videoplayer.PlayerState
|
import ru.shadowsparky.videoplayer.PlayerState
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
|
|
||||||
private const val DEF_SEEK_TIME = 10L
|
private const val DEF_SEEK_TIME = 10L
|
||||||
|
|
||||||
@@ -77,6 +69,7 @@ fun DefaultControls(
|
|||||||
controlsState: ControlsState,
|
controlsState: ControlsState,
|
||||||
controller: PlayerController,
|
controller: PlayerController,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
requestInteraction: () -> Unit,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
val isTv = isTv()
|
val isTv = isTv()
|
||||||
@@ -90,11 +83,8 @@ fun DefaultControls(
|
|||||||
val currentUiPosition = if (isDragging) sliderPosition else state.currentPositionMs.toFloat()
|
val currentUiPosition = if (isDragging) sliderPosition else state.currentPositionMs.toFloat()
|
||||||
val totalDuration = state.durationMs.toFloat().coerceAtLeast(1f)
|
val totalDuration = state.durationMs.toFloat().coerceAtLeast(1f)
|
||||||
|
|
||||||
val focus = remember { FocusContainer() }
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier.fillMaxSize()
|
||||||
.fillMaxSize()
|
|
||||||
.background(if (isUiLocked.value) Color.Transparent else Color.Black.copy(alpha = 0.4f))
|
.background(if (isUiLocked.value) Color.Transparent else Color.Black.copy(alpha = 0.4f))
|
||||||
.safeDrawingPadding()
|
.safeDrawingPadding()
|
||||||
) {
|
) {
|
||||||
@@ -105,19 +95,19 @@ fun DefaultControls(
|
|||||||
Row(modifier = Modifier.align(Alignment.TopEnd).focusGroup().padding(8.dp)) {
|
Row(modifier = Modifier.align(Alignment.TopEnd).focusGroup().padding(8.dp)) {
|
||||||
if (audioTracksState.isNotEmpty() && !isUiLocked.value) {
|
if (audioTracksState.isNotEmpty() && !isUiLocked.value) {
|
||||||
AudioTracksButton(
|
AudioTracksButton(
|
||||||
focus,
|
|
||||||
tracks = audioTracksState,
|
tracks = audioTracksState,
|
||||||
onSelect = { id ->
|
onSelect = { id ->
|
||||||
scope.launch { controller.selectAudioTrack(id) }
|
scope.launch { controller.selectAudioTrack(id) }
|
||||||
}
|
},
|
||||||
|
requestInteraction = requestInteraction
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!isTv) {
|
if (!isTv) {
|
||||||
LockUiButton(focus, isUiLocked.value) { isUiLocked.value = it }
|
LockUiButton(isUiLocked.value) { isUiLocked.value = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isUiLocked.value) {
|
if (!isUiLocked.value) {
|
||||||
CenterControls(focus, state, controller)
|
CenterControls(state, controller)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomCenter)
|
.align(Alignment.BottomCenter)
|
||||||
@@ -153,26 +143,7 @@ fun DefaultControls(
|
|||||||
controller.seekTo(sliderPosition.toLong())
|
controller.seekTo(sliderPosition.toLong())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth().focusBorder()
|
||||||
.focusableWithBorder(focus.slider)
|
|
||||||
.onPreviewKeyEvent {
|
|
||||||
if (it.type == KeyEventType.KeyDown) {
|
|
||||||
when (it.key) {
|
|
||||||
Key.DirectionLeft -> {
|
|
||||||
scope.launch { controller.seek(true) }
|
|
||||||
}
|
|
||||||
|
|
||||||
Key.DirectionRight -> {
|
|
||||||
scope.launch { controller.seek(false) }
|
|
||||||
}
|
|
||||||
|
|
||||||
Key.DirectionUp -> focus.play.requestFocus()
|
|
||||||
Key.DirectionDown -> focus.audioTrack.requestFocus()
|
|
||||||
else -> return@onPreviewKeyEvent false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,14 +152,10 @@ fun DefaultControls(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LockUiButton(
|
private fun LockUiButton(
|
||||||
focusContainer: FocusContainer,
|
|
||||||
state: Boolean,
|
state: Boolean,
|
||||||
onChanged: (Boolean) -> Unit
|
onChanged: (Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(onClick = { onChanged(!state) }) {
|
||||||
onClick = { onChanged(!state) },
|
|
||||||
modifier = Modifier.focusableWithBorder(focusContainer.lock)
|
|
||||||
) {
|
|
||||||
val icon = if (state) Res.drawable.lock_24px else Res.drawable.lock_open_24px
|
val icon = if (state) Res.drawable.lock_24px else Res.drawable.lock_open_24px
|
||||||
Icon(painterResource(icon), null, tint = Color.White)
|
Icon(painterResource(icon), null, tint = Color.White)
|
||||||
}
|
}
|
||||||
@@ -196,39 +163,34 @@ private fun LockUiButton(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AudioTracksButton(
|
private fun AudioTracksButton(
|
||||||
focusContainer: FocusContainer,
|
|
||||||
tracks: List<AudioTrackInfo>,
|
tracks: List<AudioTrackInfo>,
|
||||||
onSelect: (String) -> Unit,
|
onSelect: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
|
requestInteraction: () -> Unit
|
||||||
) {
|
) {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
val requester = remember { FocusRequester() }
|
||||||
|
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
val tracks = tracks.map { track -> track to FocusRequester() }
|
IconButton(onClick = { expanded = true }) {
|
||||||
IconButton(
|
|
||||||
onClick = { expanded = true },
|
|
||||||
modifier = Modifier.focusableWithBorder(focusContainer.audioTrack)
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(Res.drawable.music_note_24px),
|
painter = painterResource(Res.drawable.music_note_24px),
|
||||||
contentDescription = "Audio tracks",
|
contentDescription = "Audio tracks",
|
||||||
tint = Color.White
|
tint = Color.White
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
LaunchedEffect(expanded) {
|
|
||||||
if (expanded) {
|
|
||||||
delay(500.milliseconds)
|
|
||||||
tracks.firstOrNull()?.second?.requestFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = { expanded = false },
|
onDismissRequest = { expanded = false },
|
||||||
modifier = Modifier.focusable()
|
modifier = Modifier.focusable()
|
||||||
) {
|
) {
|
||||||
for (i in tracks.indices) {
|
tracks.forEachIndexed { index, track ->
|
||||||
val (track, requester) = tracks[i]
|
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
|
modifier = if (index == 0) {
|
||||||
|
Modifier.focusRequester(requester)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}.onFocusChanged { requestInteraction() },
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
text = if (track.isSelected)
|
text = if (track.isSelected)
|
||||||
@@ -240,11 +202,16 @@ private fun AudioTracksButton(
|
|||||||
onClick = {
|
onClick = {
|
||||||
expanded = false
|
expanded = false
|
||||||
onSelect(track.id)
|
onSelect(track.id)
|
||||||
},
|
}
|
||||||
modifier = Modifier.focusableWithBorder(requester)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LaunchedEffect(expanded) {
|
||||||
|
if (expanded) {
|
||||||
|
withFrameNanos {}
|
||||||
|
requester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +253,6 @@ private fun BoxScope.SideDoubleTapSeekZones(controller: PlayerController, enable
|
|||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun BoxScope.CenterControls(
|
private fun BoxScope.CenterControls(
|
||||||
focusContainer: FocusContainer,
|
|
||||||
state: PlayerState,
|
state: PlayerState,
|
||||||
controller: PlayerController
|
controller: PlayerController
|
||||||
) {
|
) {
|
||||||
@@ -328,7 +294,7 @@ private fun BoxScope.CenterControls(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
if (state.isPlaying) controller.pause() else controller.play()
|
if (state.isPlaying) controller.pause() else controller.play()
|
||||||
}
|
}
|
||||||
}, modifier = Modifier.focusableWithBorder(focusContainer.play)
|
}
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
@@ -348,12 +314,6 @@ private fun BoxScope.CenterControls(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
LaunchedEffect(state.videoUrl) { canRequestFocus = false }
|
LaunchedEffect(state.videoUrl) { canRequestFocus = false }
|
||||||
LaunchedEffect(canRequestFocus) {
|
|
||||||
if (canRequestFocus) {
|
|
||||||
withFrameNanos {}
|
|
||||||
focusContainer.play.requestFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun PlayerController.seek(minus: Boolean) {
|
private suspend fun PlayerController.seek(minus: Boolean) {
|
||||||
@@ -386,18 +346,3 @@ private fun formatDuration(millis: Long): String {
|
|||||||
|
|
||||||
return "$h:$m:$s"
|
return "$h:$m:$s"
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FocusContainer(
|
|
||||||
val audioTrack: FocusRequester = FocusRequester(),
|
|
||||||
val lock: FocusRequester = FocusRequester(),
|
|
||||||
val play: FocusRequester = FocusRequester(),
|
|
||||||
val slider: FocusRequester = FocusRequester()
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Modifier.focusableWithBorder(requester: FocusRequester): Modifier {
|
|
||||||
var isFocused by remember { mutableStateOf(false) }
|
|
||||||
return this.focusRequester(requester)
|
|
||||||
.onFocusChanged { isFocused = it.isFocused }
|
|
||||||
.focusBorder(false, CircleShape)
|
|
||||||
}
|
|
||||||
|
|||||||
+20
-5
@@ -47,17 +47,24 @@ import ru.shadowsparky.videoplayer.PlayerEvent
|
|||||||
import ru.shadowsparky.videoplayer.PlayerState
|
import ru.shadowsparky.videoplayer.PlayerState
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
class OverlayHolder(
|
||||||
|
val playerState: PlayerState,
|
||||||
|
val controlsState: ControlsState,
|
||||||
|
val requestInteraction: () -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoPlayer(
|
fun VideoPlayer(
|
||||||
controller: PlayerController,
|
controller: PlayerController,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
overlayContent: @Composable (PlayerState, ControlsState) -> Unit = { player, controls ->
|
overlayContent: @Composable (OverlayHolder) -> Unit = {
|
||||||
DefaultControls(
|
DefaultControls(
|
||||||
player,
|
it.playerState,
|
||||||
controls,
|
it.controlsState,
|
||||||
controller = controller,
|
controller = controller,
|
||||||
onBack = onBack
|
onBack = onBack,
|
||||||
|
requestInteraction = it.requestInteraction
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@@ -135,7 +142,15 @@ fun VideoPlayer(
|
|||||||
enter = fadeIn(),
|
enter = fadeIn(),
|
||||||
exit = fadeOut(),
|
exit = fadeOut(),
|
||||||
modifier = Modifier.matchParentSize()
|
modifier = Modifier.matchParentSize()
|
||||||
) { overlayContent(state, controlsState) }
|
) {
|
||||||
|
overlayContent(
|
||||||
|
OverlayHolder(
|
||||||
|
state,
|
||||||
|
controlsState,
|
||||||
|
requestInteraction = { interactionCount++ }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ object ComponentDefaults {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Modifier.focusBorder(
|
fun Modifier.focusBorder(
|
||||||
needScale: Boolean = true,
|
needScale: Boolean = false,
|
||||||
shape: Shape = RoundedCornerShape(12.dp)
|
shape: Shape = RoundedCornerShape(12.dp)
|
||||||
): Modifier {
|
): Modifier {
|
||||||
return ComponentDefaults.focusBorder(
|
return ComponentDefaults.focusBorder(
|
||||||
|
|||||||
Reference in New Issue
Block a user