summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Michal Brzezinski <brzezinski@google.com> 2024-08-01 14:15:39 +0100
committer Michal Brzezinski <brzezinski@google.com> 2024-08-02 12:45:56 +0100
commit56c42ef02b3e65c85183a7cfddb4e4ea8b796c82 (patch)
treeaa3080cd42e0c6c4523bb7f543c66ee7ba2878bf
parent43f5be5f2b318a96a87c87bafca70cb3aeed9219 (diff)
Extracting generic part of GestureTutorialScreen
Extracting GestureTutorialScreen from BackGestureTutorialScreen so it can be reused for other gestures. Now all resources, colors, animations are provided as TutorialScreenConfig Bug: 346576895 Test: Just refactoring, no logic change Flag: com.android.systemui.shared.new_touchpad_gestures_tutorial Change-Id: I7e56619fb5d723ab2f0bc5870fe228db03a93b5f
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt296
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt299
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialScreenConfig.kt48
3 files changed, 378 insertions, 265 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
index 3dca6fc65034..1b00ae2103f0 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
@@ -16,130 +16,51 @@
package com.android.systemui.touchpad.tutorial.ui.composable
-import android.graphics.ColorFilter
-import android.graphics.PorterDuff
-import android.graphics.PorterDuffColorFilter
-import androidx.activity.compose.BackHandler
-import androidx.annotation.RawRes
-import androidx.annotation.StringRes
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.animateColorAsState
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.snap
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.togetherWith
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.input.pointer.pointerInteropFilter
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import com.airbnb.lottie.LottieProperty
-import com.airbnb.lottie.compose.LottieAnimation
-import com.airbnb.lottie.compose.LottieCompositionSpec
-import com.airbnb.lottie.compose.LottieConstants
-import com.airbnb.lottie.compose.LottieDynamicProperties
-import com.airbnb.lottie.compose.LottieDynamicProperty
-import com.airbnb.lottie.compose.animateLottieCompositionAsState
-import com.airbnb.lottie.compose.rememberLottieComposition
import com.airbnb.lottie.compose.rememberLottieDynamicProperties
-import com.airbnb.lottie.compose.rememberLottieDynamicProperty
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.systemui.res.R
import com.android.systemui.touchpad.tutorial.ui.gesture.BackGestureMonitor
import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
-import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
-import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
-import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
-import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler
-
-data class TutorialScreenColors(
- val backgroundColor: Color,
- val successBackgroundColor: Color,
- val titleColor: Color,
- val animationProperties: LottieDynamicProperties
-)
+import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureMonitor
@Composable
fun BackGestureTutorialScreen(
onDoneButtonClicked: () -> Unit,
onBack: () -> Unit,
) {
- val screenColors = rememberScreenColors()
- BackHandler(onBack = onBack)
- var gestureState by remember { mutableStateOf(NOT_STARTED) }
- val swipeDistanceThresholdPx =
- LocalContext.current.resources.getDimensionPixelSize(
- com.android.internal.R.dimen.system_gestures_distance_threshold
- )
- val gestureHandler =
- remember(swipeDistanceThresholdPx) {
- TouchpadGestureHandler(
- BackGestureMonitor(
- swipeDistanceThresholdPx,
- gestureStateChangedCallback = { gestureState = it }
+ val screenConfig =
+ TutorialScreenConfig(
+ colors = rememberScreenColors(),
+ strings =
+ TutorialScreenConfig.Strings(
+ titleResId = R.string.touchpad_back_gesture_action_title,
+ bodyResId = R.string.touchpad_back_gesture_guidance,
+ titleSuccessResId = R.string.touchpad_tutorial_gesture_done,
+ bodySuccessResId = R.string.touchpad_back_gesture_finished
),
- )
- }
- TouchpadGesturesHandlingBox(gestureHandler, gestureState) {
- GestureTutorialContent(gestureState, onDoneButtonClicked, screenColors)
- }
-}
-
-@Composable
-private fun TouchpadGesturesHandlingBox(
- gestureHandler: TouchpadGestureHandler,
- gestureState: GestureState,
- modifier: Modifier = Modifier,
- content: @Composable BoxScope.() -> Unit
-) {
- Box(
- modifier =
- modifier
- .fillMaxSize()
- // we need to use pointerInteropFilter because some info about touchpad gestures is
- // only available in MotionEvent
- .pointerInteropFilter(
- onTouchEvent = { event ->
- // FINISHED is the final state so we don't need to process touches anymore
- if (gestureState != FINISHED) {
- gestureHandler.onMotionEvent(event)
- } else {
- false
- }
- }
+ animations =
+ TutorialScreenConfig.Animations(
+ educationResId = R.raw.trackpad_back_edu,
+ successResId = R.raw.trackpad_back_success
)
- ) {
- content()
- }
+ )
+ val gestureMonitorProvider =
+ object : GestureMonitorProvider {
+ override fun createGestureMonitor(
+ gestureDistanceThresholdPx: Int,
+ gestureStateChangedCallback: (GestureState) -> Unit
+ ): TouchpadGestureMonitor {
+ return BackGestureMonitor(gestureDistanceThresholdPx, gestureStateChangedCallback)
+ }
+ }
+ GestureTutorialScreen(screenConfig, gestureMonitorProvider, onDoneButtonClicked, onBack)
}
@Composable
-private fun rememberScreenColors(): TutorialScreenColors {
+private fun rememberScreenColors(): TutorialScreenConfig.Colors {
val onTertiary = LocalAndroidColorScheme.current.onTertiary
val onTertiaryFixed = LocalAndroidColorScheme.current.onTertiaryFixed
val onTertiaryFixedVariant = LocalAndroidColorScheme.current.onTertiaryFixedVariant
@@ -154,167 +75,12 @@ private fun rememberScreenColors(): TutorialScreenColors {
)
val screenColors =
remember(onTertiaryFixed, surfaceContainer, tertiaryFixedDim, dynamicProperties) {
- TutorialScreenColors(
- backgroundColor = onTertiaryFixed,
- successBackgroundColor = surfaceContainer,
- titleColor = tertiaryFixedDim,
- animationProperties = dynamicProperties,
+ TutorialScreenConfig.Colors(
+ background = onTertiaryFixed,
+ successBackground = surfaceContainer,
+ title = tertiaryFixedDim,
+ animationColors = dynamicProperties,
)
}
return screenColors
}
-
-@Composable
-private fun GestureTutorialContent(
- gestureState: GestureState,
- onDoneButtonClicked: () -> Unit,
- screenColors: TutorialScreenColors
-) {
- val animatedColor by
- animateColorAsState(
- targetValue =
- if (gestureState == FINISHED) screenColors.successBackgroundColor
- else screenColors.backgroundColor,
- animationSpec = tween(durationMillis = 150, easing = LinearEasing),
- label = "backgroundColor"
- )
- Column(
- verticalArrangement = Arrangement.Center,
- modifier =
- Modifier.fillMaxSize()
- .drawBehind { drawRect(animatedColor) }
- .padding(start = 48.dp, top = 124.dp, end = 48.dp, bottom = 48.dp)
- ) {
- Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
- TutorialDescription(
- titleTextId =
- if (gestureState == FINISHED) R.string.touchpad_tutorial_gesture_done
- else R.string.touchpad_back_gesture_action_title,
- titleColor = screenColors.titleColor,
- bodyTextId =
- if (gestureState == FINISHED) R.string.touchpad_back_gesture_finished
- else R.string.touchpad_back_gesture_guidance,
- modifier = Modifier.weight(1f)
- )
- Spacer(modifier = Modifier.width(76.dp))
- TutorialAnimation(
- gestureState,
- screenColors.animationProperties,
- modifier = Modifier.weight(1f).padding(top = 8.dp)
- )
- }
- DoneButton(onDoneButtonClicked = onDoneButtonClicked)
- }
-}
-
-@Composable
-fun TutorialDescription(
- @StringRes titleTextId: Int,
- titleColor: Color,
- @StringRes bodyTextId: Int,
- modifier: Modifier = Modifier
-) {
- Column(verticalArrangement = Arrangement.Top, modifier = modifier) {
- Text(
- text = stringResource(id = titleTextId),
- style = MaterialTheme.typography.displayLarge,
- color = titleColor
- )
- Spacer(modifier = Modifier.height(16.dp))
- Text(
- text = stringResource(id = bodyTextId),
- style = MaterialTheme.typography.bodyLarge,
- color = Color.White
- )
- }
-}
-
-@Composable
-fun TutorialAnimation(
- gestureState: GestureState,
- animationProperties: LottieDynamicProperties,
- modifier: Modifier = Modifier
-) {
- Box(modifier = modifier.fillMaxWidth()) {
- AnimatedContent(
- targetState = gestureState,
- transitionSpec = {
- if (initialState == NOT_STARTED && targetState == IN_PROGRESS) {
- val transitionDurationMillis = 150
- fadeIn(
- animationSpec = tween(transitionDurationMillis, easing = LinearEasing)
- ) togetherWith
- fadeOut(animationSpec = snap(delayMillis = transitionDurationMillis))
- } else {
- // empty transition works because all remaining transitions are from IN_PROGRESS
- // state which shares initial animation frame with both FINISHED and NOT_STARTED
- EnterTransition.None togetherWith ExitTransition.None
- }
- }
- ) { gestureState ->
- @RawRes val successAnimationId = R.raw.trackpad_back_success
- @RawRes val educationAnimationId = R.raw.trackpad_back_edu
- when (gestureState) {
- NOT_STARTED -> EducationAnimation(educationAnimationId, animationProperties)
- IN_PROGRESS -> FrozenSuccessAnimation(successAnimationId, animationProperties)
- FINISHED -> SuccessAnimation(successAnimationId, animationProperties)
- }
- }
- }
-}
-
-@Composable
-private fun FrozenSuccessAnimation(
- @RawRes successAnimationId: Int,
- animationProperties: LottieDynamicProperties
-) {
- val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(successAnimationId))
- LottieAnimation(
- composition = composition,
- progress = { 0f }, // animation should freeze on 1st frame
- dynamicProperties = animationProperties,
- )
-}
-
-@Composable
-private fun EducationAnimation(
- @RawRes educationAnimationId: Int,
- animationProperties: LottieDynamicProperties
-) {
- val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(educationAnimationId))
- val progress by
- animateLottieCompositionAsState(composition, iterations = LottieConstants.IterateForever)
- LottieAnimation(
- composition = composition,
- progress = { progress },
- dynamicProperties = animationProperties,
- )
-}
-
-@Composable
-private fun SuccessAnimation(
- @RawRes successAnimationId: Int,
- animationProperties: LottieDynamicProperties
-) {
- val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(successAnimationId))
- val progress by animateLottieCompositionAsState(composition, iterations = 1)
- LottieAnimation(
- composition = composition,
- progress = { progress },
- dynamicProperties = animationProperties,
- )
-}
-
-@Composable
-fun rememberColorFilterProperty(
- layerName: String,
- color: Color
-): LottieDynamicProperty<ColorFilter> {
- return rememberLottieDynamicProperty(
- LottieProperty.COLOR_FILTER,
- value = PorterDuffColorFilter(color.toArgb(), PorterDuff.Mode.SRC_ATOP),
- // "**" below means match zero or more layers, so ** layerName ** means find layer with that
- // name at any depth
- keyPath = arrayOf("**", layerName, "**")
- )
-}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
new file mode 100644
index 000000000000..416c562d212d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.touchpad.tutorial.ui.composable
+
+import android.graphics.ColorFilter
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import androidx.activity.compose.BackHandler
+import androidx.annotation.RawRes
+import androidx.annotation.StringRes
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.snap
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.input.pointer.pointerInteropFilter
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.LottieProperty
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.LottieConstants
+import com.airbnb.lottie.compose.LottieDynamicProperties
+import com.airbnb.lottie.compose.LottieDynamicProperty
+import com.airbnb.lottie.compose.animateLottieCompositionAsState
+import com.airbnb.lottie.compose.rememberLottieComposition
+import com.airbnb.lottie.compose.rememberLottieDynamicProperty
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
+import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler
+import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureMonitor
+
+interface GestureMonitorProvider {
+ fun createGestureMonitor(
+ gestureDistanceThresholdPx: Int,
+ gestureStateChangedCallback: (GestureState) -> Unit
+ ): TouchpadGestureMonitor
+}
+
+@Composable
+fun GestureTutorialScreen(
+ screenConfig: TutorialScreenConfig,
+ gestureMonitorProvider: GestureMonitorProvider,
+ onDoneButtonClicked: () -> Unit,
+ onBack: () -> Unit,
+) {
+ BackHandler(onBack = onBack)
+ var gestureState by remember { mutableStateOf(NOT_STARTED) }
+ val swipeDistanceThresholdPx =
+ LocalContext.current.resources.getDimensionPixelSize(
+ com.android.internal.R.dimen.system_gestures_distance_threshold
+ )
+ val gestureHandler =
+ remember(swipeDistanceThresholdPx) {
+ TouchpadGestureHandler(
+ gestureMonitorProvider.createGestureMonitor(
+ swipeDistanceThresholdPx,
+ gestureStateChangedCallback = { gestureState = it }
+ )
+ )
+ }
+ TouchpadGesturesHandlingBox(gestureHandler, gestureState) {
+ GestureTutorialContent(gestureState, onDoneButtonClicked, screenConfig)
+ }
+}
+
+@Composable
+private fun TouchpadGesturesHandlingBox(
+ gestureHandler: TouchpadGestureHandler,
+ gestureState: GestureState,
+ modifier: Modifier = Modifier,
+ content: @Composable BoxScope.() -> Unit
+) {
+ Box(
+ modifier =
+ modifier
+ .fillMaxSize()
+ // we need to use pointerInteropFilter because some info about touchpad gestures is
+ // only available in MotionEvent
+ .pointerInteropFilter(
+ onTouchEvent = { event ->
+ // FINISHED is the final state so we don't need to process touches anymore
+ if (gestureState == FINISHED) {
+ false
+ } else {
+ gestureHandler.onMotionEvent(event)
+ }
+ }
+ )
+ ) {
+ content()
+ }
+}
+
+@Composable
+private fun GestureTutorialContent(
+ gestureState: GestureState,
+ onDoneButtonClicked: () -> Unit,
+ config: TutorialScreenConfig
+) {
+ val animatedColor by
+ animateColorAsState(
+ targetValue =
+ if (gestureState == FINISHED) config.colors.successBackground
+ else config.colors.background,
+ animationSpec = tween(durationMillis = 150, easing = LinearEasing),
+ label = "backgroundColor"
+ )
+ Column(
+ verticalArrangement = Arrangement.Center,
+ modifier =
+ Modifier.fillMaxSize()
+ .drawBehind { drawRect(animatedColor) }
+ .padding(start = 48.dp, top = 124.dp, end = 48.dp, bottom = 48.dp)
+ ) {
+ Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
+ TutorialDescription(
+ titleTextId =
+ if (gestureState == FINISHED) config.strings.titleSuccessResId
+ else config.strings.titleResId,
+ titleColor = config.colors.title,
+ bodyTextId =
+ if (gestureState == FINISHED) config.strings.bodySuccessResId
+ else config.strings.bodyResId,
+ modifier = Modifier.weight(1f)
+ )
+ Spacer(modifier = Modifier.width(76.dp))
+ TutorialAnimation(
+ gestureState,
+ config,
+ modifier = Modifier.weight(1f).padding(top = 8.dp)
+ )
+ }
+ DoneButton(onDoneButtonClicked = onDoneButtonClicked)
+ }
+}
+
+@Composable
+fun TutorialDescription(
+ @StringRes titleTextId: Int,
+ titleColor: Color,
+ @StringRes bodyTextId: Int,
+ modifier: Modifier = Modifier
+) {
+ Column(verticalArrangement = Arrangement.Top, modifier = modifier) {
+ Text(
+ text = stringResource(id = titleTextId),
+ style = MaterialTheme.typography.displayLarge,
+ color = titleColor
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = stringResource(id = bodyTextId),
+ style = MaterialTheme.typography.bodyLarge,
+ color = Color.White
+ )
+ }
+}
+
+@Composable
+fun TutorialAnimation(
+ gestureState: GestureState,
+ config: TutorialScreenConfig,
+ modifier: Modifier = Modifier
+) {
+ Box(modifier = modifier.fillMaxWidth()) {
+ AnimatedContent(
+ targetState = gestureState,
+ transitionSpec = {
+ if (initialState == NOT_STARTED && targetState == IN_PROGRESS) {
+ val transitionDurationMillis = 150
+ fadeIn(
+ animationSpec = tween(transitionDurationMillis, easing = LinearEasing)
+ ) togetherWith
+ fadeOut(animationSpec = snap(delayMillis = transitionDurationMillis))
+ } else {
+ // empty transition works because all remaining transitions are from IN_PROGRESS
+ // state which shares initial animation frame with both FINISHED and NOT_STARTED
+ EnterTransition.None togetherWith ExitTransition.None
+ }
+ }
+ ) { gestureState ->
+ when (gestureState) {
+ NOT_STARTED ->
+ EducationAnimation(
+ config.animations.educationResId,
+ config.colors.animationColors
+ )
+ IN_PROGRESS ->
+ FrozenSuccessAnimation(
+ config.animations.successResId,
+ config.colors.animationColors
+ )
+ FINISHED ->
+ SuccessAnimation(config.animations.successResId, config.colors.animationColors)
+ }
+ }
+ }
+}
+
+@Composable
+private fun FrozenSuccessAnimation(
+ @RawRes successAnimationId: Int,
+ animationProperties: LottieDynamicProperties
+) {
+ val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(successAnimationId))
+ LottieAnimation(
+ composition = composition,
+ progress = { 0f }, // animation should freeze on 1st frame
+ dynamicProperties = animationProperties,
+ )
+}
+
+@Composable
+private fun EducationAnimation(
+ @RawRes educationAnimationId: Int,
+ animationProperties: LottieDynamicProperties
+) {
+ val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(educationAnimationId))
+ val progress by
+ animateLottieCompositionAsState(composition, iterations = LottieConstants.IterateForever)
+ LottieAnimation(
+ composition = composition,
+ progress = { progress },
+ dynamicProperties = animationProperties,
+ )
+}
+
+@Composable
+private fun SuccessAnimation(
+ @RawRes successAnimationId: Int,
+ animationProperties: LottieDynamicProperties
+) {
+ val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(successAnimationId))
+ val progress by animateLottieCompositionAsState(composition, iterations = 1)
+ LottieAnimation(
+ composition = composition,
+ progress = { progress },
+ dynamicProperties = animationProperties,
+ )
+}
+
+@Composable
+fun rememberColorFilterProperty(
+ layerName: String,
+ color: Color
+): LottieDynamicProperty<ColorFilter> {
+ return rememberLottieDynamicProperty(
+ LottieProperty.COLOR_FILTER,
+ value = PorterDuffColorFilter(color.toArgb(), PorterDuff.Mode.SRC_ATOP),
+ // "**" below means match zero or more layers, so ** layerName ** means find layer with that
+ // name at any depth
+ keyPath = arrayOf("**", layerName, "**")
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialScreenConfig.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialScreenConfig.kt
new file mode 100644
index 000000000000..d76ceb9380cd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialScreenConfig.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.touchpad.tutorial.ui.composable
+
+import androidx.annotation.RawRes
+import androidx.annotation.StringRes
+import androidx.compose.ui.graphics.Color
+import com.airbnb.lottie.compose.LottieDynamicProperties
+
+data class TutorialScreenConfig(
+ val colors: Colors,
+ val strings: Strings,
+ val animations: Animations
+) {
+
+ data class Colors(
+ val background: Color,
+ val successBackground: Color,
+ val title: Color,
+ val animationColors: LottieDynamicProperties
+ )
+
+ data class Strings(
+ @StringRes val titleResId: Int,
+ @StringRes val bodyResId: Int,
+ @StringRes val titleSuccessResId: Int,
+ @StringRes val bodySuccessResId: Int,
+ )
+
+ data class Animations(
+ @RawRes val educationResId: Int,
+ @RawRes val successResId: Int,
+ )
+}