Skip to content

Commit

Permalink
Merge pull request #1082 from kul3r4/speaker-sample
Browse files Browse the repository at this point in the history
Removes media playing from speaker sample
  • Loading branch information
kul3r4 authored May 3, 2024
2 parents e147fc0 + 148a796 commit cfb46bf
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 245 deletions.
7 changes: 4 additions & 3 deletions WearSpeakerSample/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ WearSpeakerSample
================================

A sample that shows how you can record voice using the microphone on a wearable and
play the recorded voice or an mp3 file, if the wearable device is connected to a speaker
(bluetooth or built-in).
play the recorded voice, if the wearable device is connected to a speaker
(bluetooth or built-in). If you want to learn how to play media content on Wear OS, refer to
[Horologist Media sample](https://google.github.io/horologist/media-sample/)

This sample is also written entirely with Jetpack Compose, using [Wear-specific components provided
by AndroidX](https://developer.android.com/jetpack/androidx/releases/wear-compose).
The Compose UI is fairly advanced, using [ConstraintLayout for Compose](https://developer.android.com/jetpack/compose/layouts/constraintlayout) and animations.
The sample also handles permissions and uses the [effect APIs](https://developer.android.com/jetpack/compose/side-effects) where necessary.
If you are looking for a simple example of Compose on [WearOS], refer to [ComposeStarter](../ComposeStarter).
If you are looking for a simpler example of Compose on [WearOS], refer to [ComposeStarter](../ComposeStarter).

This sample doesn't have any companion phone app so you need to install this directly
on your watch using `adb` or Android Studio.
Expand Down
38 changes: 30 additions & 8 deletions WearSpeakerSample/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,49 @@ androidx-compose-bom = "2024.05.00"
androidx-lifecycle = "2.7.0"
androidx-wear-compose = "1.3.1"
compose-compiler = "1.5.12"
compose-ui-tooling = "1.3.1"
ktlint = "0.50.0"
org-jetbrains-kotlin = "1.9.23"
org-jetbrains-kotlinx = "1.8.0"
robolectric = "4.12.1"
roborazzi = "1.12.0"
ui-test-junit4 = "1.6.5"
ui-test-manifest = "1.6.5"
horologist = "0.5.26"
androidx-constraintlayout-compose = "1.0.1"

[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidx-compose-bom" }
androidx-constraintlayout-compose = "androidx.constraintlayout:constraintlayout-compose:1.0.1"
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
androidx-compose-ui-tooling = { module = "androidx.wear.compose:compose-ui-tooling", version.ref = "compose-ui-tooling" }
androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core" }
androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "ui-test-junit4" }
androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "ui-test-manifest" }
androidx-media = "androidx.media:media:1.7.0"
compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "compose-compiler" }
compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
compose-material-ripple = { module = "androidx.compose.material:material-ripple" }
compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
jacoco-ant = "org.jacoco:org.jacoco.ant:0.8.12"
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
desugar-jdk-libs = "com.android.tools:desugar_jdk_libs:2.0.4"
horologist-composables = { module = "com.google.android.horologist:horologist-composables", version.ref = "horologist" }
horologist-compose-layout = { module = "com.google.android.horologist:horologist-compose-layout", version.ref = "horologist" }
horologist-compose-material = { module = "com.google.android.horologist:horologist-compose-material", version.ref = "horologist" }
horologist-roboscreenshots = { module = "com.google.android.horologist:horologist-roboscreenshots", version.ref = "horologist" }
junit = "junit:junit:4.13.2"
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "org-jetbrains-kotlin" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "org-jetbrains-kotlinx" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
roborazzi = { group = "io.github.takahirom.roborazzi", name = "roborazzi", version.ref = "roborazzi" }
roborazzi-compose = { group = "io.github.takahirom.roborazzi", name = "roborazzi-compose", version.ref = "roborazzi" }
roborazzi-rule = { group = "io.github.takahirom.roborazzi", name = "roborazzi-junit-rule", version.ref = "roborazzi" }
test-espresso-core = "androidx.test.espresso:espresso-core:3.5.1"
test-ext-junit = "androidx.test.ext:junit:1.1.5"
wear-compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "androidx-wear-compose" }
wear-compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "androidx-wear-compose" }
wear-compose-navigation = { module = "androidx.wear.compose:compose-navigation", version.ref = "androidx-wear-compose" }
androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "androidx-constraintlayout-compose" }
compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }

[plugins]
com-android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
com-diffplug-spotless = "com.diffplug.spotless:6.25.0"
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "org-jetbrains-kotlin" }
roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" }

83 changes: 70 additions & 13 deletions WearSpeakerSample/wear/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ android {
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
coreLibraryDesugaringEnabled true
}

kotlinOptions {
Expand All @@ -61,15 +62,71 @@ android {
dependencies {
def composeBom = platform(libs.androidx.compose.bom)

implementation composeBom
implementation libs.kotlinx.coroutines.android
implementation libs.androidx.activity.compose
implementation libs.compose.material.ripple
implementation libs.compose.material.icons.extended
implementation libs.compose.ui.tooling
implementation libs.androidx.constraintlayout.compose
implementation libs.androidx.lifecycle.runtime.ktx
implementation libs.androidx.media
implementation libs.wear.compose.material
implementation libs.wear.compose.foundation

// Horologist for correct Compose layout
implementation libs.horologist.composables
implementation libs.horologist.compose.layout
implementation libs.horologist.compose.material

// General compose dependencies
implementation composeBom
implementation libs.androidx.activity.compose

// Compose for Wear OS Dependencies
// NOTE: DO NOT INCLUDE a dependency on androidx.compose.material:material.
// androidx.wear.compose:compose-material is designed as a replacement not an addition to
// androidx.compose.material:material. If there are features from that you feel are missing from
// androidx.wear.compose:compose-material please raise a bug to let us know:
// https://issuetracker.google.com/issues/new?component=1077552&template=1598429&pli=1
implementation libs.wear.compose.material
implementation libs.compose.material.icons.extended

coreLibraryDesugaring libs.desugar.jdk.libs


// Foundation is additive, so you can use the mobile version in your Wear OS app.
implementation libs.wear.compose.foundation
implementation(libs.androidx.material.icons.core)

// Horologist for correct Compose layout
implementation libs.horologist.composables
implementation libs.horologist.compose.layout
implementation libs.horologist.compose.material

// Preview Tooling
implementation libs.compose.ui.tooling.preview
implementation(libs.androidx.compose.ui.tooling)

implementation libs.androidx.media

// If you are using Compose Navigation, use the Wear OS version (NOT the
// androidx.navigation:navigation-compose version), that is, uncomment the line below.
implementation libs.wear.compose.navigation

implementation libs.androidx.ui.test.manifest

implementation libs.androidx.constraintlayout.compose

coreLibraryDesugaring libs.desugar.jdk.libs

// Testing
testImplementation libs.androidx.ui.test.junit4
testImplementation libs.junit
testImplementation libs.robolectric
testImplementation libs.roborazzi
testImplementation libs.roborazzi.compose
testImplementation libs.roborazzi.rule
testImplementation(libs.horologist.roboscreenshots) {
exclude(group: "com.github.QuickBirdEng.kotlin-snapshot-testing")
}

androidTestImplementation libs.test.ext.junit
androidTestImplementation libs.test.espresso.core
androidTestImplementation libs.compose.ui.test.junit4
androidTestImplementation composeBom

debugImplementation libs.compose.ui.tooling
debugImplementation libs.androidx.ui.test.manifest
debugImplementation composeBom

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(ExperimentalHorologistApi::class)

package com.example.android.wearable.speaker

import androidx.compose.animation.core.animateDpAsState
Expand All @@ -22,7 +24,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.MusicNote
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
Expand All @@ -37,6 +38,7 @@ import androidx.constraintlayout.compose.ConstraintSet
import androidx.constraintlayout.compose.Dimension
import androidx.wear.compose.material.Button
import androidx.wear.compose.material.Icon
import com.google.android.horologist.annotations.ExperimentalHorologistApi

/**
* The component responsible for drawing the main 3 controls, with their expanded and minimized
Expand All @@ -50,20 +52,17 @@ fun ControlDashboard(
controlDashboardUiState: ControlDashboardUiState,
onMicClicked: () -> Unit,
onPlayClicked: () -> Unit,
onMusicClicked: () -> Unit,
modifier: Modifier = Modifier
) {
val circle = Any()
val mic = Any()
val play = Any()
val music = Any()

val constraintSet = createConstraintSet(
controlDashboardUiState = controlDashboardUiState,
circle = circle,
mic = mic,
play = play,
music = music
play = play
)

// We are using ConstraintLayout here for the circular constraints
Expand All @@ -73,7 +72,7 @@ fun ControlDashboard(
modifier = modifier
) {
Spacer(
modifier = Modifier.layoutId(circle)
modifier = modifier.layoutId(circle)
)

ControlDashboardButton(
Expand All @@ -99,18 +98,6 @@ fun ControlDashboard(
stringResource(id = R.string.play_recording)
}
)

ControlDashboardButton(
buttonState = controlDashboardUiState.musicState,
onClick = onMusicClicked,
layoutId = music,
imageVector = Icons.Filled.MusicNote,
contentDescription = if (controlDashboardUiState.musicState.expanded) {
stringResource(id = R.string.stop_playing_music)
} else {
stringResource(id = R.string.play_music)
}
)
}
}

Expand All @@ -124,8 +111,7 @@ private fun createConstraintSet(
controlDashboardUiState: ControlDashboardUiState,
circle: Any,
mic: Any,
play: Any,
music: Any
play: Any
): ConstraintSet {
val iconCircleRadius = 32.dp
val iconMinimizedSize = 48.dp
Expand Down Expand Up @@ -153,38 +139,21 @@ private fun createConstraintSet(
targetValue = if (controlDashboardUiState.playState.expanded) 0.dp else iconCircleRadius
)

val musicSize by animateDpAsState(
targetValue = if (controlDashboardUiState.musicState.expanded) {
iconExpandedSize
} else {
iconMinimizedSize
}
)
val musicRadius by animateDpAsState(
targetValue = if (controlDashboardUiState.musicState.expanded) 0.dp else iconCircleRadius
)

return ConstraintSet {
val circleRef = createRefFor(circle)
val micRef = createRefFor(mic)
val playRef = createRefFor(play)
val musicRef = createRefFor(music)

constrain(circleRef) { centerTo(parent) }
constrain(micRef) {
width = Dimension.value(micSize)
height = Dimension.value(micSize)
circular(circleRef, 0f, micRadius)
}
constrain(playRef) {
width = Dimension.value(playSize)
height = Dimension.value(playSize)
circular(circleRef, 240f, playRadius)
circular(circleRef, 90f, playRadius)
}
constrain(musicRef) {
width = Dimension.value(musicSize)
height = Dimension.value(musicSize)
circular(circleRef, 120f, musicRadius)
constrain(micRef) {
width = Dimension.value(micSize)
height = Dimension.value(micSize)
circular(circleRef, 270f, micRadius)
}
}
}
Expand All @@ -198,7 +167,8 @@ private fun ControlDashboardButton(
onClick: () -> Unit,
layoutId: Any,
imageVector: ImageVector,
contentDescription: String
contentDescription: String,
modifier: Modifier = Modifier
) {
val iconPadding = 8.dp
// TODO: Replace with a version of IconButton?
Expand All @@ -209,7 +179,7 @@ private fun ControlDashboardButton(
)

Button(
modifier = Modifier
modifier = modifier
.fillMaxSize()
.alpha(alpha)
.layoutId(layoutId),
Expand Down Expand Up @@ -240,16 +210,14 @@ data class ControlDashboardButtonUiState(
*/
data class ControlDashboardUiState(
val micState: ControlDashboardButtonUiState,
val playState: ControlDashboardButtonUiState,
val musicState: ControlDashboardButtonUiState
val playState: ControlDashboardButtonUiState
) {
init {
// Check that at most one of the buttons is expanded
require(
listOf(
micState.expanded,
playState.expanded,
musicState.expanded
playState.expanded
).count { it } <= 1
)
}
Expand Down
Loading

0 comments on commit cfb46bf

Please sign in to comment.