Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: Handle ambient mode in Exercise screens #317

Merged
merged 5 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions health-services/ExerciseSampleCompose/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ plugins {
}

android {
compileSdk 34
compileSdk 35

defaultConfig {
applicationId "com.example.exercisecompose"
Expand Down Expand Up @@ -132,9 +132,7 @@ dependencies {
testImplementation libs.roborazzi
testImplementation libs.roborazzi.compose
testImplementation libs.roborazzi.rule
testImplementation(libs.horologist.roboscreenshots) {
exclude(group: "com.github.QuickBirdEng.kotlin-snapshot-testing")
}
testImplementation(libs.horologist.roboscreenshots)

androidTestImplementation libs.test.ext.junit
androidTestImplementation libs.test.espresso.core
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,11 @@ package com.example.exercisesamplecompose.presentation

import ExerciseGoalsRoute
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.navArgument
import androidx.wear.compose.navigation.SwipeDismissableNavHost
import androidx.wear.compose.navigation.composable
import androidx.wear.compose.navigation.currentBackStackEntryAsState
import com.example.exercisesamplecompose.app.Screen
import com.example.exercisesamplecompose.app.Screen.Exercise
import com.example.exercisesamplecompose.app.Screen.ExerciseNotAvailable
Expand All @@ -35,94 +33,74 @@ import com.example.exercisesamplecompose.presentation.dialogs.ExerciseNotAvailab
import com.example.exercisesamplecompose.presentation.exercise.ExerciseRoute
import com.example.exercisesamplecompose.presentation.preparing.PreparingExerciseRoute
import com.example.exercisesamplecompose.presentation.summary.SummaryRoute
import com.google.android.horologist.compose.ambient.AmbientAware
import com.google.android.horologist.compose.ambient.AmbientState
import com.google.android.horologist.compose.layout.AppScaffold
import com.google.android.horologist.compose.layout.ResponsiveTimeText

/** Navigation for the exercise app. **/
@Composable
fun ExerciseSampleApp(
navController: NavHostController,
onFinishActivity: () -> Unit
) {
val currentScreen by navController.currentBackStackEntryAsState()
AppScaffold {
SwipeDismissableNavHost(
navController = navController,
startDestination = Exercise.route,

val isAlwaysOnScreen = currentScreen?.destination?.route in AlwaysOnRoutes

AmbientAware(
isAlwaysOnScreen = isAlwaysOnScreen
) { ambientStateUpdate ->

AppScaffold(
timeText = {
if (ambientStateUpdate.ambientState is AmbientState.Interactive) {
ResponsiveTimeText()
}
}
) {
SwipeDismissableNavHost(
navController = navController,
startDestination = Exercise.route,

) {
composable(PreparingExercise.route) {
PreparingExerciseRoute(
ambientState = ambientStateUpdate.ambientState,
onStart = {
navController.navigate(Exercise.route) {
popUpTo(navController.graph.id) {
inclusive = false
}
) {
composable(PreparingExercise.route) {
PreparingExerciseRoute(
onStart = {
navController.navigate(Exercise.route) {
popUpTo(navController.graph.id) {
inclusive = false
}
},
onNoExerciseCapabilities = {
navController.navigate(ExerciseNotAvailable.route) {
popUpTo(navController.graph.id) {
inclusive = false
}
}
},
onNoExerciseCapabilities = {
navController.navigate(ExerciseNotAvailable.route) {
popUpTo(navController.graph.id) {
inclusive = false
}
},
onFinishActivity = onFinishActivity,
onGoals = { navController.navigate(Screen.Goals.route) }
)
}
}
},
onFinishActivity = onFinishActivity,
onGoals = { navController.navigate(Screen.Goals.route) }
)
}

composable(Exercise.route) {
ExerciseRoute(
ambientState = ambientStateUpdate.ambientState,
onSummary = {
navController.navigateToTopLevel(Summary, Summary.buildRoute(it))
},
onRestart = {
navController.navigateToTopLevel(PreparingExercise)
},
onFinishActivity = onFinishActivity
)
}
composable(Exercise.route) {
ExerciseRoute(
onSummary = {
navController.navigateToTopLevel(Summary, Summary.buildRoute(it))
},
onRestart = {
navController.navigateToTopLevel(PreparingExercise)
},
onFinishActivity = onFinishActivity
)
}

composable(ExerciseNotAvailable.route) {
ExerciseNotAvailable()
}
composable(ExerciseNotAvailable.route) {
ExerciseNotAvailable()
}

composable(
Summary.route + "/{averageHeartRate}/{totalDistance}/{totalCalories}/{elapsedTime}",
arguments = listOf(
navArgument(Summary.averageHeartRateArg) { type = NavType.FloatType },
navArgument(Summary.totalDistanceArg) { type = NavType.FloatType },
navArgument(Summary.totalCaloriesArg) { type = NavType.FloatType },
navArgument(Summary.elapsedTimeArg) { type = NavType.StringType }
)
) {
SummaryRoute(
onRestartClick = {
navController.navigateToTopLevel(PreparingExercise)
}
)
}
composable(Screen.Goals.route) {
ExerciseGoalsRoute(onSet = { navController.popBackStack() })
}
composable(
Summary.route + "/{averageHeartRate}/{totalDistance}/{totalCalories}/{elapsedTime}",
arguments = listOf(
navArgument(Summary.averageHeartRateArg) { type = NavType.FloatType },
navArgument(Summary.totalDistanceArg) { type = NavType.FloatType },
navArgument(Summary.totalCaloriesArg) { type = NavType.FloatType },
navArgument(Summary.elapsedTimeArg) { type = NavType.StringType }
)
) {
SummaryRoute(
onRestartClick = {
navController.navigateToTopLevel(PreparingExercise)
}
)
}
composable(Screen.Goals.route) {
ExerciseGoalsRoute(onSet = { navController.popBackStack() })
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.example.exercisesamplecompose.presentation.ambient

import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.withSaveLayer
import com.google.android.horologist.compose.ambient.AmbientState

/**
* A Paint object configured to apply a grayscale effect.
*
* This is achieved by using a ColorMatrix to set the saturation to 0,
* effectively removing all color information from the image.
* Anti-aliasing is disabled for this paint to potentially improve performance.
*/
private val grayscale = Paint().apply {
colorFilter = ColorFilter.colorMatrix(
ColorMatrix().apply {
setToSaturation(0f)
}
)
isAntiAlias = false
}

/**
* Applies a grayscale effect and scales down the content when in ambient mode.
*
* This modifier checks the provided [AmbientState] to determine if the device is
* in ambient mode. If it is, the content is scaled down by 10% and a grayscale
* filter is applied. When not in ambient mode, the content is rendered normally.
*/
fun Modifier.ambientGray(ambientState: AmbientState): Modifier =
graphicsLayer {
if (ambientState.isAmbient) {
scaleX = 0.9f
scaleY = 0.9f
}
}.drawWithContent {
if (ambientState.isAmbient) {
drawIntoCanvas {
it.withSaveLayer(size.toRect(), grayscale) {
drawContent()
}
}
} else {
drawContent()
}
}

/**
* This modifier conditionally draws the content based on the state provided by an [AmbientState].
*
* If the `isInteractive` property of the provided [ambientState] is true, the content will be drawn.
* Otherwise, the content will not be drawn, effectively leaving the area blank.
*/
fun Modifier.ambientBlank(ambientState: AmbientState): Modifier =
drawWithContent {
if (ambientState.isInteractive) {
drawContent()
}
}

This file was deleted.

Loading
Loading