Skip to content

Commit fae68cf

Browse files
Merge pull request #23 from android/prateek/add-tabletop-support
Prateek/add tabletop support
2 parents 6f85554 + caac322 commit fae68cf

5 files changed

Lines changed: 257 additions & 44 deletions

File tree

AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt

Lines changed: 99 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@ import androidx.compose.foundation.background
2323
import androidx.compose.foundation.clickable
2424
import androidx.compose.foundation.focusGroup
2525
import androidx.compose.foundation.layout.Box
26+
import androidx.compose.foundation.layout.Column
2627
import androidx.compose.foundation.layout.fillMaxSize
28+
import androidx.compose.foundation.layout.fillMaxWidth
2729
import androidx.compose.foundation.layout.padding
2830
import androidx.compose.foundation.shape.RoundedCornerShape
2931
import androidx.compose.material3.MaterialTheme
32+
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
3033
import androidx.compose.runtime.Composable
3134
import androidx.compose.runtime.LaunchedEffect
3235
import androidx.compose.runtime.getValue
@@ -193,8 +196,12 @@ private fun VideoPlayer(
193196

194197
// TODO: Move to ViewModel for better reuse
195198
val pulseState = rememberVideoPlayerPulseState()
196-
197-
val videoPlayerState = rememberVideoPlayerState(player = player, hideSeconds = 4)
199+
val isTabletopMode = currentWindowAdaptiveInfo().windowPosture.isTabletop
200+
val videoPlayerState = rememberVideoPlayerState(
201+
player = player,
202+
isTabletopMode = isTabletopMode,
203+
hideSeconds = 4
204+
)
198205
val videoSize by videoPlayerState
199206
.videoSize
200207
.collectAsStateWithLifecycle(Size(1980f, 1080f))
@@ -214,9 +221,11 @@ private fun VideoPlayer(
214221
!hasXrSpatialFeature -> {
215222
activity?.toggleImmersiveMode()
216223
}
224+
217225
isSpatialUiEnabled -> {
218226
spatialConfiguration.requestHomeSpaceMode()
219227
}
228+
220229
else -> {
221230
spatialConfiguration.requestFullSpaceMode()
222231
}
@@ -392,23 +401,102 @@ private fun VideoPlayer2D(
392401
modifier: Modifier = Modifier,
393402
onBackPressed: () -> Unit = {}
394403
) {
395-
val focusRequester = remember { FocusRequester() }
396-
397-
val size = if(videoSize == Size.Zero) {
404+
val size = if (videoSize == Size.Zero) {
398405
null
399406
} else {
400407
videoSize
401408
}
402409

410+
if (videoPlayerState.isTabletopMode) {
411+
TabletopVideoPlayer(
412+
modifier = modifier,
413+
player = player,
414+
size = size,
415+
nowPlayingInfo = nowPlayingInfo,
416+
videoPlayerState = videoPlayerState,
417+
pulseState = pulseState,
418+
onBackPressed = onBackPressed
419+
)
420+
} else {
421+
DefaultVideoPlayer(
422+
modifier = modifier,
423+
player = player,
424+
size = size,
425+
nowPlayingInfo = nowPlayingInfo,
426+
videoPlayerState = videoPlayerState,
427+
pulseState = pulseState,
428+
onBackPressed = onBackPressed
429+
)
430+
}
431+
}
432+
433+
@Composable
434+
private fun TabletopVideoPlayer(
435+
modifier: Modifier = Modifier,
436+
player: Player,
437+
size: Size?,
438+
nowPlayingInfo: NowPlayingInfo,
439+
videoPlayerState: VideoPlayerState,
440+
pulseState: VideoPlayerPulseState,
441+
onBackPressed: () -> Unit
442+
) {
443+
Column(modifier = modifier.fillMaxSize()) {
444+
Box(
445+
modifier = Modifier
446+
.weight(1f)
447+
.fillMaxWidth(),
448+
contentAlignment = Alignment.Center
449+
) {
450+
PlayerSurface(
451+
player = player,
452+
modifier = Modifier
453+
.fillMaxSize()
454+
.resizeWithContentScale(
455+
contentScale = ContentScale.Fit,
456+
sourceSizeDp = size
457+
)
458+
)
459+
}
460+
Box(
461+
modifier = Modifier
462+
.weight(1f)
463+
.fillMaxWidth(),
464+
contentAlignment = Alignment.Center
465+
) {
466+
VideoPlayerControlsInOverlay(
467+
movieDetails = nowPlayingInfo.movieDetails,
468+
player = player,
469+
videoPlayerState = videoPlayerState,
470+
videoPlayerPulseState = pulseState,
471+
onBackPressed = onBackPressed,
472+
modifier = Modifier
473+
)
474+
}
475+
}
476+
}
477+
478+
@Composable
479+
private fun DefaultVideoPlayer(
480+
modifier: Modifier = Modifier,
481+
player: Player,
482+
size: Size?,
483+
nowPlayingInfo: NowPlayingInfo,
484+
videoPlayerState: VideoPlayerState,
485+
pulseState: VideoPlayerPulseState,
486+
onBackPressed: () -> Unit
487+
) {
488+
val focusRequester = remember { FocusRequester() }
403489
Box(
404-
modifier = modifier
490+
modifier = modifier.fillMaxSize()
405491
) {
406492
PlayerSurface(
407493
player = player,
408-
modifier = Modifier.resizeWithContentScale(
409-
contentScale = ContentScale.Fit,
410-
sourceSizeDp = size
411-
)
494+
modifier = Modifier
495+
.fillMaxSize()
496+
.resizeWithContentScale(
497+
contentScale = ContentScale.Fit,
498+
sourceSizeDp = size
499+
)
412500
)
413501
VideoPlayerControlsInOverlay(
414502
movieDetails = nowPlayingInfo.movieDetails,
@@ -541,4 +629,4 @@ private fun Modifier.dPadEvents(
541629
player.pause()
542630
videoPlayerState.showControls()
543631
}
544-
)
632+
)

AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerControls.kt

Lines changed: 95 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ package com.google.jetstream.presentation.screens.videoPlayer.components
1818

1919
import androidx.compose.foundation.horizontalScroll
2020
import androidx.compose.foundation.layout.Arrangement
21+
import androidx.compose.foundation.layout.Column
2122
import androidx.compose.foundation.layout.Row
23+
import androidx.compose.foundation.layout.fillMaxWidth
2224
import androidx.compose.foundation.layout.padding
25+
import androidx.compose.foundation.layout.size
2326
import androidx.compose.foundation.rememberScrollState
27+
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
2428
import androidx.compose.runtime.Composable
2529
import androidx.compose.runtime.LaunchedEffect
2630
import androidx.compose.runtime.remember
@@ -36,11 +40,17 @@ import com.google.jetstream.presentation.screens.videoPlayer.components.button.C
3640
import com.google.jetstream.presentation.screens.videoPlayer.components.button.ImmersiveModeButton
3741
import com.google.jetstream.presentation.screens.videoPlayer.components.button.NextButton
3842
import com.google.jetstream.presentation.screens.videoPlayer.components.button.PlayListButton
43+
import com.google.jetstream.presentation.screens.videoPlayer.components.button.PlayPauseButton
3944
import com.google.jetstream.presentation.screens.videoPlayer.components.button.PreviousButton
4045
import com.google.jetstream.presentation.screens.videoPlayer.components.button.RepeatButton
4146
import com.google.jetstream.presentation.screens.videoPlayer.components.button.SettingsButton
4247
import kotlin.time.Duration.Companion.milliseconds
4348

49+
private val DefaultActionSpacing = 12.dp
50+
private val TabletopActionSpacing = 15.dp
51+
private val TabletopButtonSize = 70.dp
52+
private val BottomPadding = 16.dp
53+
4454
@Composable
4555
fun VideoPlayerControls(
4656
movieDetails: MovieDetails,
@@ -53,45 +63,111 @@ fun VideoPlayerControls(
5363
focusRequester.tryRequestFocus()
5464
}
5565

56-
val scrollState = rememberScrollState()
66+
val isTabletop = currentWindowAdaptiveInfo().windowPosture.isTabletop
5767

5868
VideoPlayerMainFrame(
5969
mediaTitle = {
6070
VideoPlayerMediaTitle(
6171
title = movieDetails.name,
6272
secondaryText = movieDetails.releaseDate,
6373
tertiaryText = movieDetails.director,
64-
type = VideoPlayerMediaTitleType.DEFAULT
74+
type = VideoPlayerMediaTitleType.DEFAULT,
75+
modifier = Modifier.fillMaxWidth()
6576
)
6677
},
6778
mediaActions = {
68-
Row(
69-
modifier = Modifier
70-
.padding(bottom = 16.dp)
71-
.horizontalScroll(scrollState),
72-
verticalAlignment = Alignment.CenterVertically,
73-
horizontalArrangement = Arrangement.spacedBy(12.dp),
74-
) {
75-
PreviousButton(player = player)
76-
NextButton(player = player)
77-
RepeatButton(player = player)
78-
PlayListButton()
79-
ClosedCaptionButton()
80-
SettingsButton()
81-
if (isImmersiveModeAvailable) {
82-
ImmersiveModeButton()
83-
}
79+
if (isTabletop) {
80+
TabletopMediaActions(player, isImmersiveModeAvailable)
81+
} else {
82+
DefaultMediaActions(player, isImmersiveModeAvailable)
8483
}
8584
},
8685
seeker = {
8786
VideoPlayerSeeker(
8887
player = player,
8988
onSeek = { player.seekTo(player.duration.times(it).toLong()) },
9089
contentDuration = player.duration.milliseconds,
91-
modifier = Modifier.focusRequester(focusRequester)
90+
modifier = Modifier.focusRequester(focusRequester),
91+
shouldShowPlayPauseButton = !isTabletop
9292
)
9393
},
9494
more = null,
9595
modifier = modifier
9696
)
9797
}
98+
99+
@Composable
100+
private fun TabletopMediaActions(
101+
player: Player,
102+
isImmersiveModeAvailable: Boolean,
103+
) {
104+
Column(
105+
modifier = Modifier.fillMaxWidth(),
106+
horizontalAlignment = Alignment.CenterHorizontally
107+
) {
108+
Row(
109+
modifier = Modifier
110+
.padding(bottom = BottomPadding)
111+
.weight(1f),
112+
horizontalArrangement = Arrangement.spacedBy(
113+
TabletopActionSpacing,
114+
Alignment.CenterHorizontally
115+
),
116+
verticalAlignment = Alignment.CenterVertically
117+
) {
118+
PreviousButton(player = player, modifier = Modifier.size(TabletopButtonSize))
119+
PlayPauseButton(player = player, modifier = Modifier.size(TabletopButtonSize))
120+
NextButton(player = player, modifier = Modifier.size(TabletopButtonSize))
121+
}
122+
PlayerActions(
123+
player = player,
124+
isImmersiveModeAvailable = isImmersiveModeAvailable,
125+
modifier = Modifier.weight(1f),
126+
horizontalArrangement = Arrangement.spacedBy(
127+
TabletopActionSpacing,
128+
Alignment.CenterHorizontally
129+
)
130+
)
131+
}
132+
}
133+
134+
@Composable
135+
private fun DefaultMediaActions(
136+
player: Player,
137+
isImmersiveModeAvailable: Boolean,
138+
) {
139+
val scrollState = rememberScrollState()
140+
Row(
141+
modifier = Modifier
142+
.padding(bottom = BottomPadding)
143+
.horizontalScroll(scrollState),
144+
verticalAlignment = Alignment.CenterVertically,
145+
horizontalArrangement = Arrangement.spacedBy(DefaultActionSpacing),
146+
) {
147+
PreviousButton(player = player)
148+
NextButton(player = player)
149+
PlayerActions(player, isImmersiveModeAvailable)
150+
}
151+
}
152+
153+
@Composable
154+
private fun PlayerActions(
155+
player: Player,
156+
isImmersiveModeAvailable: Boolean,
157+
modifier: Modifier = Modifier,
158+
horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(DefaultActionSpacing)
159+
) {
160+
Row(
161+
modifier = modifier,
162+
horizontalArrangement = horizontalArrangement,
163+
verticalAlignment = Alignment.CenterVertically
164+
) {
165+
RepeatButton(player = player)
166+
PlayListButton()
167+
ClosedCaptionButton()
168+
SettingsButton()
169+
if (isImmersiveModeAvailable) {
170+
ImmersiveModeButton()
171+
}
172+
}
173+
}

AdaptiveJetStream/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerMainFrame.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ fun VideoPlayerMainFrame(
4646
windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
4747
) {
4848
when {
49+
currentWindowAdaptiveInfo().windowPosture.isTabletop -> {
50+
TabletopVideoPlayerFrame(
51+
mediaTitle = mediaTitle,
52+
seeker = seeker,
53+
mediaActions = mediaActions,
54+
more = more
55+
)
56+
}
57+
4958
windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND) -> {
5059
NonCompactVideoPlayerFrame(
5160
mediaTitle = mediaTitle,
@@ -54,6 +63,7 @@ fun VideoPlayerMainFrame(
5463
more = more
5564
)
5665
}
66+
5767
else -> {
5868
CompactVideoPlayerFrame(
5969
mediaTitle = mediaTitle,
@@ -85,6 +95,35 @@ private fun CompactVideoPlayerFrame(
8595
}
8696
}
8797

98+
@Composable
99+
private fun TabletopVideoPlayerFrame(
100+
mediaTitle: @Composable () -> Unit,
101+
seeker: @Composable () -> Unit,
102+
mediaActions: @Composable () -> Unit = {},
103+
more: (@Composable () -> Unit)? = null
104+
) {
105+
Column(modifier = Modifier.fillMaxWidth()) {
106+
Spacer(Modifier.height(16.dp))
107+
Row(modifier = Modifier.fillMaxWidth()) {
108+
seeker()
109+
}
110+
Spacer(Modifier.height(16.dp))
111+
Row {
112+
mediaTitle()
113+
}
114+
Spacer(Modifier.height(16.dp))
115+
Row {
116+
mediaActions()
117+
}
118+
if (more != null) {
119+
Spacer(Modifier.height(12.dp))
120+
Row {
121+
more()
122+
}
123+
}
124+
}
125+
}
126+
88127
@Composable
89128
private fun NonCompactVideoPlayerFrame(
90129
mediaTitle: @Composable () -> Unit,

0 commit comments

Comments
 (0)