Skip to content

Commit

Permalink
Refactor: Handle ambient mode in Exercise screens (#317)
Browse files Browse the repository at this point in the history
* Refactor: Handle ambient mode in Exercise screens

This commit introduces changes to handle ambient mode within individual exercise screens instead of globally.

- An AmbientAware composable is added to each screen to control ambient behavior.
- Modifiers for ambient mode (ambientGray, ambientBlank) are applied within the screen composables.
- Removed the ambientState parameter from composables and adjusted accordingly.
  • Loading branch information
yschimke authored Dec 6, 2024
1 parent 12ce555 commit 6430cfd
Show file tree
Hide file tree
Showing 45 changed files with 367 additions and 382 deletions.
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

0 comments on commit 6430cfd

Please sign in to comment.