Skip to content

Commit 0dd78bf

Browse files
committed
Adds Playback speed
1 parent 0bfe3ae commit 0dd78bf

File tree

17 files changed

+335
-39
lines changed

17 files changed

+335
-39
lines changed

Jetcaster/core/src/main/java/com/example/jetcaster/core/player/EpisodePlayer.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,14 @@ interface EpisodePlayer {
102102
* Rewinds a currently played episode by a given time interval specified in [duration].
103103
*/
104104
fun rewindBy(duration: Duration)
105+
106+
/**
107+
* Increases the speed of Player playback by a given time specified in [duration].
108+
*/
109+
fun increaseSpeed(speed: Duration = Duration.ofMillis(500))
110+
111+
/**
112+
* Decreases the speed of Player playback by a given time specified in [duration].
113+
*/
114+
fun decreaseSpeed(speed: Duration = Duration.ofMillis(500))
105115
}

Jetcaster/core/src/main/java/com/example/jetcaster/core/player/MockEpisodePlayer.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,14 @@ class MockEpisodePlayer(
173173
}
174174
}
175175

176+
override fun increaseSpeed(speed: Duration) {
177+
_playerSpeed.value += Duration.ofMillis(500)
178+
}
179+
180+
override fun decreaseSpeed(speed: Duration) {
181+
_playerSpeed.value -= Duration.ofMillis(500)
182+
}
183+
176184
override fun next() {
177185
val q = queue.value
178186
if (q.isEmpty()) {

Jetcaster/gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ composeMaterial = "1.2.1"
6161
composeFoundation = "1.2.1"
6262
coreSplashscreen = "1.0.1"
6363
horologistComposeTools = "0.4.8"
64-
horologist = "0.6.6"
64+
horologist = "0.6.9"
6565
roborazzi = "1.11.0"
6666
androidx-wear-compose = "1.3.0"
6767
wear-compose-ui-tooling = "1.3.0"

Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ import com.example.jetcaster.theme.WearAppTheme
2828
import com.example.jetcaster.ui.Episode
2929
import com.example.jetcaster.ui.JetcasterNavController.navigateToEpisode
3030
import com.example.jetcaster.ui.JetcasterNavController.navigateToLatestEpisode
31+
import com.example.jetcaster.ui.JetcasterNavController.navigateToPlaybackSpeed
3132
import com.example.jetcaster.ui.JetcasterNavController.navigateToPodcastDetails
3233
import com.example.jetcaster.ui.JetcasterNavController.navigateToUpNext
3334
import com.example.jetcaster.ui.JetcasterNavController.navigateToYourPodcast
3435
import com.example.jetcaster.ui.LatestEpisodes
36+
import com.example.jetcaster.ui.PlaybackSpeed
3537
import com.example.jetcaster.ui.PodcastDetails
3638
import com.example.jetcaster.ui.UpNext
3739
import com.example.jetcaster.ui.YourPodcasts
@@ -40,6 +42,7 @@ import com.example.jetcaster.ui.home.HomeScreen
4042
import com.example.jetcaster.ui.library.LatestEpisodesScreen
4143
import com.example.jetcaster.ui.library.PodcastsScreen
4244
import com.example.jetcaster.ui.library.QueueScreen
45+
import com.example.jetcaster.ui.player.PlaybackSpeedScreen
4346
import com.example.jetcaster.ui.player.PlayerScreen
4447
import com.example.jetcaster.ui.podcast.PodcastDetailsScreen
4548
import com.google.android.horologist.audio.ui.VolumeViewModel
@@ -69,6 +72,9 @@ fun WearApp() {
6972
onVolumeClick = {
7073
navController.navigateToVolume()
7174
},
75+
onPlaybackSpeedChangeClick = {
76+
navController.navigateToPlaybackSpeed()
77+
},
7278
)
7379
},
7480
libraryScreen = {
@@ -137,6 +143,9 @@ fun WearApp() {
137143
}
138144
)
139145
}
146+
composable(route = PlaybackSpeed.navRoute) {
147+
PlaybackSpeedScreen()
148+
}
140149
},
141150

142151
)

Jetcaster/wear/src/main/java/com/example/jetcaster/ui/JetcasterNavController.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ public object JetcasterNavController {
4747
public fun NavController.navigateToEpisode(episodeUri: String) {
4848
navigate(Episode.destination(episodeUri))
4949
}
50+
51+
public fun NavController.navigateToPlaybackSpeed() {
52+
navigate(PlaybackSpeed.destination())
53+
}
5054
}
5155

5256
public object YourPodcasts : NavigationScreens("yourPodcasts") {
@@ -90,3 +94,7 @@ public object Episode : NavigationScreens("episode?episodeUri={episodeUri}") {
9094
public object UpNext : NavigationScreens("upNext") {
9195
public fun destination(): String = navRoute
9296
}
97+
98+
public object PlaybackSpeed : NavigationScreens("playbackSpeed") {
99+
public fun destination(): String = navRoute
100+
}

Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/SettingsButtons.kt

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ package com.example.jetcaster.ui.components
1919
import androidx.compose.foundation.layout.Arrangement
2020
import androidx.compose.foundation.layout.Row
2121
import androidx.compose.foundation.layout.fillMaxWidth
22-
import androidx.compose.material.icons.Icons
23-
import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
2422
import androidx.compose.runtime.Composable
2523
import androidx.compose.ui.Alignment
2624
import androidx.compose.ui.Modifier
25+
import androidx.compose.ui.graphics.vector.ImageVector
2726
import androidx.compose.ui.res.stringResource
27+
import androidx.compose.ui.res.vectorResource
2828
import com.example.jetcaster.R
29+
import com.example.jetcaster.ui.player.PlayerUiState
2930
import com.google.android.horologist.audio.ui.VolumeUiState
3031
import com.google.android.horologist.audio.ui.components.SettingsButtonsDefaults
3132
import com.google.android.horologist.audio.ui.components.actions.SetVolumeButton
@@ -40,7 +41,8 @@ import com.google.android.horologist.compose.material.IconRtlMode
4041
fun SettingsButtons(
4142
volumeUiState: VolumeUiState,
4243
onVolumeClick: () -> Unit,
43-
onAddToQueueClick: () -> Unit,
44+
playerUiState: PlayerUiState,
45+
onPlaybackSpeedChange: () -> Unit,
4446
modifier: Modifier = Modifier,
4547
enabled: Boolean = true,
4648
) {
@@ -49,8 +51,11 @@ fun SettingsButtons(
4951
verticalAlignment = Alignment.CenterVertically,
5052
horizontalArrangement = Arrangement.SpaceEvenly,
5153
) {
52-
AddToQueueButton(
53-
onAddToQueueClick = onAddToQueueClick,
54+
PlaybackSpeedButton(
55+
currentPlayerSpeed = playerUiState.episodePlayerState
56+
.playbackSpeed.toMillis().toFloat() / 1000,
57+
onPlaybackSpeedChange = onPlaybackSpeedChange,
58+
enabled = enabled
5459
)
5560

5661
SettingsButtonsDefaults.BrandIcon(
@@ -61,22 +66,29 @@ fun SettingsButtons(
6166
SetVolumeButton(
6267
onVolumeClick = onVolumeClick,
6368
volumeUiState = volumeUiState,
69+
enabled = enabled
6470
)
6571
}
6672
}
6773

6874
@Composable
69-
fun AddToQueueButton(
70-
onAddToQueueClick: () -> Unit,
75+
fun PlaybackSpeedButton(
76+
currentPlayerSpeed: Float,
77+
onPlaybackSpeedChange: () -> Unit,
7178
modifier: Modifier = Modifier,
7279
enabled: Boolean = true,
7380
) {
7481
SettingsButton(
7582
modifier = modifier,
76-
onClick = onAddToQueueClick,
83+
onClick = onPlaybackSpeedChange,
7784
enabled = enabled,
78-
imageVector = Icons.AutoMirrored.Filled.PlaylistAdd,
85+
imageVector =
86+
when (currentPlayerSpeed) {
87+
1f -> ImageVector.vectorResource(R.drawable.speed_1x)
88+
1.5f -> ImageVector.vectorResource(R.drawable.speed_15x)
89+
else -> { ImageVector.vectorResource(R.drawable.speed_2x) }
90+
},
7991
iconRtlMode = IconRtlMode.Mirrored,
80-
contentDescription = stringResource(R.string.add_to_queue_content_description),
92+
contentDescription = stringResource(R.string.change_playback_speed_content_description),
8193
)
8294
}

Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/QueueScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ fun QueueScreen(
158158
showDialog = true,
159159
onDismiss = onDismiss,
160160
title = stringResource(R.string.display_nothing_in_queue),
161-
message = stringResource(R.string.failed_loading_episodes_from_queue)
161+
message = stringResource(R.string.no_episodes_from_queue)
162162
)
163163
}
164164
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright 2024 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.jetcaster.ui.player
18+
19+
import androidx.compose.foundation.layout.padding
20+
import androidx.compose.runtime.Composable
21+
import androidx.compose.runtime.CompositionLocalProvider
22+
import androidx.compose.runtime.collectAsState
23+
import androidx.compose.runtime.getValue
24+
import androidx.compose.runtime.setValue
25+
import androidx.compose.ui.Modifier
26+
import androidx.compose.ui.res.stringResource
27+
import androidx.hilt.navigation.compose.hiltViewModel
28+
import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
29+
import androidx.wear.compose.material.ContentAlpha
30+
import androidx.wear.compose.material.Icon
31+
import androidx.wear.compose.material.InlineSlider
32+
import androidx.wear.compose.material.InlineSliderDefaults
33+
import androidx.wear.compose.material.LocalContentAlpha
34+
import androidx.wear.compose.material.Text
35+
import com.example.jetcaster.R
36+
import com.google.android.horologist.compose.layout.ScalingLazyColumn
37+
import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults
38+
import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults.listTextPadding
39+
import com.google.android.horologist.compose.layout.ScreenScaffold
40+
import com.google.android.horologist.compose.layout.rememberResponsiveColumnState
41+
import com.google.android.horologist.compose.material.Chip
42+
import com.google.android.horologist.compose.material.ResponsiveListHeader
43+
44+
/**
45+
* Playback Speed Screen with an [InlineSlider].
46+
*/
47+
@OptIn(ExperimentalWearFoundationApi::class)
48+
@Composable
49+
public fun PlaybackSpeedScreen(
50+
modifier: Modifier = Modifier,
51+
playbackSpeedViewModel: PlaybackSpeedViewModel = hiltViewModel(),
52+
) {
53+
val playbackSpeedUiState by playbackSpeedViewModel.speedUiState.collectAsState()
54+
55+
val columnState = rememberResponsiveColumnState(
56+
contentPadding = ScalingLazyColumnDefaults.padding(
57+
first = ScalingLazyColumnDefaults.ItemType.Text,
58+
last = ScalingLazyColumnDefaults.ItemType.Chip,
59+
),
60+
)
61+
ScreenScaffold(scrollState = columnState) {
62+
ScalingLazyColumn(columnState = columnState) {
63+
item {
64+
ResponsiveListHeader(modifier = Modifier.listTextPadding()) {
65+
Text(stringResource(R.string.speed))
66+
}
67+
}
68+
item {
69+
PlaybackSpeedScreen(
70+
playbackSpeedUiState = playbackSpeedUiState,
71+
increasePlaybackSpeed = playbackSpeedViewModel::increaseSpeed,
72+
decreasePlaybackSpeed = playbackSpeedViewModel::decreaseSpeed,
73+
modifier = modifier
74+
)
75+
}
76+
77+
item {
78+
Text(
79+
text = String.format("%.1fx", playbackSpeedUiState.current),
80+
)
81+
}
82+
}
83+
}
84+
}
85+
86+
@Composable
87+
internal fun PlaybackSpeedScreen(
88+
playbackSpeedUiState: PlaybackSpeedUiState,
89+
increasePlaybackSpeed: () -> Unit,
90+
decreasePlaybackSpeed: () -> Unit,
91+
modifier: Modifier
92+
) {
93+
InlineSlider(
94+
value = playbackSpeedUiState.current,
95+
onValueChange = {
96+
if (it > playbackSpeedUiState.current) increasePlaybackSpeed()
97+
else if (it > 0.5) decreasePlaybackSpeed()
98+
},
99+
increaseIcon = {
100+
Icon(
101+
InlineSliderDefaults.Increase,
102+
stringResource(R.string.increase_playback_speed)
103+
)
104+
},
105+
decreaseIcon = {
106+
CompositionLocalProvider(
107+
LocalContentAlpha provides
108+
if (playbackSpeedUiState.current > 1f)
109+
LocalContentAlpha.current else ContentAlpha.disabled
110+
) {
111+
Icon(
112+
InlineSliderDefaults.Decrease,
113+
stringResource(R.string.decrease_playback_speed)
114+
)
115+
}
116+
},
117+
valueRange = 0.5f..2f,
118+
steps = 2,
119+
segmented = true
120+
)
121+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2024 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.jetcaster.ui.player
18+
19+
public data class PlaybackSpeedUiState(
20+
val current: Float = 1f
21+
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2024 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.jetcaster.ui.player
18+
19+
import java.time.Duration
20+
21+
public object PlaybackSpeedUiStateMapper {
22+
/**
23+
* Functions to map a [PlaybackSpeedUiState] from a [Duration]. The view model
24+
* uses float to represent the values displayed in the [PlayerScreen].
25+
*/
26+
public fun map(playbackSpeed: Duration): PlaybackSpeedUiState = PlaybackSpeedUiState(
27+
current = (playbackSpeed.toMillis().toFloat() / 1000)
28+
)
29+
}

0 commit comments

Comments
 (0)