Skip to content

Commit 60bb347

Browse files
committed
feat: support disabling focus on click for the player controls and passing your own focus requester
1 parent ab99907 commit 60bb347

File tree

4 files changed

+126
-78
lines changed

4 files changed

+126
-78
lines changed

src/main/kotlin/dev/silenium/multimedia/compose/player/VideoPlayerControls.kt

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,15 @@ import kotlin.time.Duration.Companion.seconds
3939
fun VideoSurfaceControls(
4040
player: VideoPlayer,
4141
modifier: Modifier = Modifier,
42+
focusRequester: FocusRequester? = null,
43+
focusOnClick: Boolean = false,
4244
) {
4345
val coroutineScope = rememberCoroutineScope()
4446
val paused by deferredFlowStateOf(player::paused)
4547
val loading by player.property<Boolean>("seeking")
4648
val backgroundColor = MaterialTheme.colors.surface
47-
val focus = remember { FocusRequester() }
49+
val focus = remember(focusRequester) { focusRequester ?: FocusRequester() }
4850
Box(modifier = modifier) {
49-
Box(
50-
modifier = Modifier.matchParentSize()
51-
.handleInputs(player, focus)
52-
.focusRequester(focus)
53-
.focusable(enabled = true, interactionSource = remember { MutableInteractionSource() })
54-
)
5551
StateIndicatorIcon(player, Modifier.align(Alignment.Center))
5652
if (loading != false) {
5753
CircularProgressIndicator(
@@ -60,6 +56,12 @@ fun VideoSurfaceControls(
6056
strokeCap = StrokeCap.Round,
6157
)
6258
}
59+
Box(
60+
modifier = Modifier.matchParentSize()
61+
.handleInputs(player, focus.takeIf { focusOnClick })
62+
.focusRequester(focus)
63+
.focusable(enabled = true, interactionSource = remember { MutableInteractionSource() })
64+
)
6365

6466
BoxWithConstraints(
6567
modifier = Modifier
@@ -151,6 +153,8 @@ fun VideoSurfaceControls(
151153
}
152154
}
153155
LaunchedEffect(focus) {
154-
focus.requestFocus()
156+
if (focusRequester == null) { // only focus if we created the focus requester
157+
focus.requestFocus()
158+
}
155159
}
156160
}

src/main/kotlin/dev/silenium/multimedia/compose/player/VideoSurfaceEvents.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import kotlin.time.Duration.Companion.milliseconds
1919

2020
@OptIn(ExperimentalComposeUiApi::class, InternalMultimediaApi::class)
2121
@Composable
22-
fun Modifier.handleInputs(player: VideoPlayer, focusRequester: FocusRequester): Modifier {
22+
fun Modifier.handleInputs(player: VideoPlayer, focusRequester: FocusRequester? = null): Modifier {
2323
val coroutineScope = rememberCoroutineScope()
2424
var setSpeedJob: Job? by remember { mutableStateOf(null) }
2525
var spedUp by remember { mutableStateOf(false) }
@@ -31,7 +31,7 @@ fun Modifier.handleInputs(player: VideoPlayer, focusRequester: FocusRequester):
3131
while (true) {
3232
val event = awaitPointerEvent()
3333
if (event.button == PointerButton.Primary) {
34-
focusRequester.requestFocus()
34+
focusRequester?.requestFocus()
3535
if (event.type == PointerEventType.Press) {
3636
setSpeedJob = coroutineScope.launch {
3737
delay(longPressTimeout)

src/main/kotlin/dev/silenium/multimedia/compose/player/VideoSurfaceWithControls.kt

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,25 @@ import androidx.compose.foundation.layout.requiredSizeIn
55
import androidx.compose.runtime.Composable
66
import androidx.compose.ui.Alignment
77
import androidx.compose.ui.Modifier
8-
import dev.silenium.multimedia.core.annotation.InternalMultimediaApi
8+
import androidx.compose.ui.focus.FocusRequester
99

10-
@OptIn(InternalMultimediaApi::class)
1110
@Composable
1211
fun VideoSurfaceWithControls(
1312
player: VideoPlayer,
1413
modifier: Modifier = Modifier,
1514
showStats: Boolean = false,
16-
onInitialized: () -> Unit = {},
15+
controlFocusRequester: FocusRequester? = null,
1716
) {
1817
BoxWithConstraints(modifier) {
19-
BoxWithConstraints(
20-
modifier = Modifier.requiredSizeIn(
21-
this@BoxWithConstraints.minWidth,
22-
this@BoxWithConstraints.minHeight,
23-
this@BoxWithConstraints.maxWidth,
24-
this@BoxWithConstraints.maxHeight,
25-
)
26-
) {
27-
VideoSurface(
28-
player, showStats,
29-
onInitialized = onInitialized,
30-
modifier = Modifier.align(Alignment.Center).requiredSizeIn(
31-
minWidth = minWidth,
32-
minHeight = minHeight,
33-
maxWidth = maxWidth,
34-
maxHeight = maxHeight,
35-
),
36-
)
37-
VideoSurfaceControls(player, Modifier.matchParentSize())
38-
}
18+
VideoSurface(
19+
player, showStats,
20+
modifier = Modifier.align(Alignment.Center).requiredSizeIn(
21+
minWidth = minWidth,
22+
minHeight = minHeight,
23+
maxWidth = maxWidth,
24+
maxHeight = maxHeight,
25+
),
26+
)
27+
VideoSurfaceControls(player, Modifier.matchParentSize(), controlFocusRequester)
3928
}
4029
}

src/test/kotlin/dev/silenium/multimedia/compose/Main.kt

Lines changed: 100 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,29 @@ package dev.silenium.multimedia.compose
33
import androidx.compose.foundation.background
44
import androidx.compose.foundation.isSystemInDarkTheme
55
import androidx.compose.foundation.layout.*
6-
import androidx.compose.foundation.rememberScrollState
7-
import androidx.compose.foundation.verticalScroll
6+
import androidx.compose.foundation.lazy.LazyColumn
7+
import androidx.compose.foundation.lazy.rememberLazyListState
88
import androidx.compose.material.MaterialTheme
99
import androidx.compose.material.Text
1010
import androidx.compose.material.darkColors
1111
import androidx.compose.material.lightColors
1212
import androidx.compose.runtime.*
13+
import androidx.compose.ui.Alignment
1314
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.focus.FocusRequester
1416
import androidx.compose.ui.unit.dp
1517
import androidx.compose.ui.window.Window
1618
import androidx.compose.ui.window.awaitApplication
1719
import dev.silenium.multimedia.compose.player.VideoSurfaceWithControls
1820
import dev.silenium.multimedia.compose.player.rememberVideoPlayer
1921
import dev.silenium.multimedia.compose.util.LocalFullscreenProvider
2022
import dev.silenium.multimedia.core.annotation.InternalMultimediaApi
21-
import kotlinx.coroutines.Dispatchers
22-
import kotlinx.coroutines.delay
23-
import kotlinx.coroutines.isActive
24-
import kotlinx.coroutines.withContext
23+
import kotlinx.coroutines.*
2524
import java.nio.file.Files
2625
import kotlin.io.path.absolutePathString
2726
import kotlin.io.path.outputStream
2827
import kotlin.time.Duration.Companion.milliseconds
28+
import kotlin.time.Duration.Companion.seconds
2929

3030
@OptIn(InternalMultimediaApi::class)
3131
@Composable
@@ -38,55 +38,110 @@ fun App() {
3838
}
3939
videoFile.apply { toFile().deleteOnExit() }
4040
}
41-
val player = rememberVideoPlayer()
4241
var ready by remember { mutableStateOf(false) }
42+
val coroutineScope = rememberCoroutineScope()
43+
val player = rememberVideoPlayer(
44+
onInitialized = {
45+
ready = true
46+
},
47+
)
48+
DisposableEffect(Unit) {
49+
onDispose {
50+
ready = false
51+
}
52+
}
53+
LaunchedEffect(file) {
54+
withContext(Dispatchers.Default) {
55+
while (!ready && isActive) delay(10.milliseconds)
56+
player.command("loadfile", file.absolutePathString())
57+
}
58+
}
59+
val fullscreen = LocalFullscreenProvider.current.isFullscreen
60+
val lazyState = rememberLazyListState()
4361
BoxWithConstraints(
44-
modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background)
62+
modifier = Modifier.background(MaterialTheme.colors.background).fillMaxSize()
4563
) {
46-
val fullscreen = LocalFullscreenProvider.current.isFullscreen
47-
val scroll = rememberScrollState()
48-
Column(
49-
modifier = Modifier.verticalScroll(scroll, !LocalFullscreenProvider.current.isFullscreen).fillMaxSize()
50-
) {
51-
VideoSurfaceWithControls(
52-
player = player,
53-
modifier = Modifier.let {
54-
when {
55-
fullscreen -> it.size(this@BoxWithConstraints.maxWidth, this@BoxWithConstraints.maxHeight)
56-
else -> it.requiredSizeIn(
57-
this@BoxWithConstraints.minWidth,
58-
this@BoxWithConstraints.minHeight,
59-
this@BoxWithConstraints.maxWidth,
60-
this@BoxWithConstraints.maxHeight,
61-
)
62-
}
63-
},
64-
showStats = true,
65-
onInitialized = {
66-
ready = true
67-
}
64+
var visible by remember { mutableStateOf(true) }
65+
var wasPaused by remember { mutableStateOf("no") }
66+
LaunchedEffect(Unit) {
67+
while (isActive) {
68+
delay(2.seconds)
69+
visible = !visible
70+
}
71+
}
72+
val modifier = when {
73+
fullscreen -> Modifier.size(
74+
this@BoxWithConstraints.maxWidth,
75+
this@BoxWithConstraints.maxHeight
6876
)
69-
Text(
70-
text = "This is a test video player",
71-
modifier = Modifier.padding(16.dp),
72-
style = MaterialTheme.typography.h6,
73-
color = MaterialTheme.colors.onBackground
77+
78+
else -> Modifier.requiredSizeIn(
79+
this@BoxWithConstraints.minWidth,
80+
this@BoxWithConstraints.minHeight,
81+
this@BoxWithConstraints.maxWidth,
82+
this@BoxWithConstraints.maxHeight,
7483
)
7584
}
76-
var previousPosition by remember { mutableStateOf(0) }
77-
LaunchedEffect(fullscreen) {
78-
if (fullscreen) {
79-
previousPosition = scroll.value
80-
scroll.scrollTo(0)
85+
LazyColumn(
86+
modifier = modifier,
87+
state = lazyState,
88+
userScrollEnabled = !fullscreen,
89+
) {
90+
if (visible) {
91+
item(key = "video", contentType = "video") {
92+
VideoSurfaceWithControls(
93+
player = player,
94+
modifier = Modifier.fillParentMaxSize().animateItem(),
95+
showStats = true,
96+
controlFocusRequester = remember { FocusRequester() },
97+
)
98+
DisposableEffect(Unit) {
99+
coroutineScope.launch {
100+
println("Setting pause to $wasPaused")
101+
player.setProperty("pause", wasPaused)
102+
}
103+
104+
onDispose {
105+
coroutineScope.launch {
106+
wasPaused = player.getProperty<String>("pause").getOrNull() ?: "no"
107+
player.setProperty("pause", "yes")
108+
}
109+
}
110+
}
111+
}
81112
} else {
82-
scroll.scrollTo(previousPosition)
113+
item(key = "video", contentType = "empty") {
114+
Box(
115+
modifier = Modifier.fillParentMaxSize().animateItem(),
116+
) {
117+
Text(
118+
text = "Video player is not visible",
119+
modifier = Modifier.padding(16.dp).align(Alignment.Center),
120+
style = MaterialTheme.typography.h6,
121+
color = MaterialTheme.colors.onBackground
122+
)
123+
}
124+
}
125+
}
126+
item(key = "text", contentType = "text") {
127+
Text(
128+
text = "This is a test video player",
129+
modifier = Modifier.padding(16.dp),
130+
style = MaterialTheme.typography.h6,
131+
color = MaterialTheme.colors.onBackground
132+
)
83133
}
84134
}
85135
}
86-
LaunchedEffect(file) {
87-
withContext(Dispatchers.Default) {
88-
while (!ready && isActive) delay(10.milliseconds)
89-
player.command("loadfile", file.absolutePathString())
136+
var previousIndex by remember { mutableStateOf(0) }
137+
var previousOffset by remember { mutableStateOf(0) }
138+
LaunchedEffect(fullscreen) {
139+
if (fullscreen) {
140+
previousIndex = lazyState.firstVisibleItemIndex
141+
previousOffset = lazyState.firstVisibleItemScrollOffset
142+
lazyState.scrollToItem(0)
143+
} else {
144+
lazyState.scrollToItem(previousIndex, previousOffset)
90145
}
91146
}
92147
}

0 commit comments

Comments
 (0)