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