1 /*
<lambda>null2 * Copyright (C) 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 * http://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.android.systemui.inputdevice.tutorial.ui.composable
18
19 import android.content.res.Configuration
20 import androidx.annotation.RawRes
21 import androidx.compose.animation.core.animateFloatAsState
22 import androidx.compose.foundation.background
23 import androidx.compose.foundation.focusable
24 import androidx.compose.foundation.layout.Arrangement
25 import androidx.compose.foundation.layout.Column
26 import androidx.compose.foundation.layout.Row
27 import androidx.compose.foundation.layout.Spacer
28 import androidx.compose.foundation.layout.fillMaxSize
29 import androidx.compose.foundation.layout.fillMaxWidth
30 import androidx.compose.foundation.layout.height
31 import androidx.compose.foundation.layout.padding
32 import androidx.compose.foundation.layout.safeDrawingPadding
33 import androidx.compose.foundation.layout.width
34 import androidx.compose.material3.MaterialTheme
35 import androidx.compose.material3.Text
36 import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
37 import androidx.compose.runtime.Composable
38 import androidx.compose.runtime.LaunchedEffect
39 import androidx.compose.runtime.getValue
40 import androidx.compose.runtime.remember
41 import androidx.compose.runtime.saveable.Saver
42 import androidx.compose.runtime.saveable.mapSaver
43 import androidx.compose.ui.Modifier
44 import androidx.compose.ui.focus.FocusRequester
45 import androidx.compose.ui.focus.focusRequester
46 import androidx.compose.ui.graphics.graphicsLayer
47 import androidx.compose.ui.platform.LocalConfiguration
48 import androidx.compose.ui.res.stringResource
49 import androidx.compose.ui.unit.dp
50 import com.android.compose.windowsizeclass.LocalWindowSizeClass
51 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error
52 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished
53 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress
54 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgressAfterError
55 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NotStarted
56 import com.android.systemui.keyboard.shortcut.ui.composable.hasCompactWindowSize
57
58 sealed interface TutorialActionState {
59 data object NotStarted : TutorialActionState
60
61 data class InProgress(
62 override val progress: Float = 0f,
63 override val startMarker: String? = null,
64 override val endMarker: String? = null,
65 ) : TutorialActionState, Progress
66
67 data class Finished(@RawRes val successAnimation: Int) : TutorialActionState
68
69 data object Error : TutorialActionState
70
71 data class InProgressAfterError(val inProgress: InProgress) :
72 TutorialActionState, Progress by inProgress
73
74 companion object {
75 fun stateSaver(): Saver<TutorialActionState, Any> {
76 val classKey = "class"
77 val successAnimationKey = "animation"
78 return mapSaver(
79 save = {
80 buildMap {
81 put(classKey, it::class.java.name)
82 if (it is Finished) put(successAnimationKey, it.successAnimation)
83 }
84 },
85 restore = { map ->
86 when (map[classKey] as? String) {
87 NotStarted::class.java.name,
88 InProgress::class.java.name -> NotStarted
89 Error::class.java.name,
90 InProgressAfterError::class.java.name -> Error
91 Finished::class.java.name -> Finished(map[successAnimationKey]!! as Int)
92 else -> NotStarted
93 }
94 },
95 )
96 }
97 }
98 }
99
100 interface Progress {
101 val progress: Float
102 val startMarker: String?
103 val endMarker: String?
104 }
105
106 @Composable
ActionTutorialContentnull107 fun ActionTutorialContent(
108 actionState: TutorialActionState,
109 onDoneButtonClicked: () -> Unit,
110 config: TutorialScreenConfig,
111 onAutoProceed: (suspend () -> Unit)? = null,
112 ) {
113 Column(
114 verticalArrangement = Arrangement.Center,
115 modifier = Modifier.fillMaxSize().background(config.colors.background).safeDrawingPadding(),
116 ) {
117 val isCompactWindow = hasCompactWindowSize()
118 when (LocalConfiguration.current.orientation) {
119 Configuration.ORIENTATION_LANDSCAPE -> {
120 HorizontalDescriptionAndAnimation(
121 actionState,
122 config,
123 isCompactWindow,
124 Modifier.weight(1f),
125 )
126 }
127 else -> {
128 VerticalDescriptionAndAnimation(
129 actionState,
130 config,
131 isCompactWindow,
132 Modifier.weight(1f),
133 )
134 }
135 }
136 val buttonAlpha by animateFloatAsState(if (actionState is Finished) 1f else 0f)
137 DoneButton(
138 onDoneButtonClicked = onDoneButtonClicked,
139 modifier = Modifier.padding(horizontal = 60.dp).graphicsLayer { alpha = buttonAlpha },
140 enabled = actionState is Finished,
141 isNext = onAutoProceed != null,
142 )
143 }
144 if (actionState is Finished) {
145 LaunchedEffect(Unit) { onAutoProceed?.invoke() }
146 }
147 }
148
149 @Composable
HorizontalDescriptionAndAnimationnull150 private fun HorizontalDescriptionAndAnimation(
151 actionState: TutorialActionState,
152 config: TutorialScreenConfig,
153 isCompactWindow: Boolean,
154 modifier: Modifier = Modifier,
155 ) {
156 Row(
157 modifier =
158 modifier.fillMaxWidth().padding(start = 48.dp, top = 100.dp, end = 48.dp, bottom = 8.dp)
159 ) {
160 TutorialDescription(actionState, config, isCompactWindow, modifier = Modifier.weight(1f))
161 Spacer(modifier = Modifier.width(24.dp))
162 TutorialAnimation(actionState, config, modifier = Modifier.weight(1f))
163 }
164 }
165
166 @Composable
VerticalDescriptionAndAnimationnull167 private fun VerticalDescriptionAndAnimation(
168 actionState: TutorialActionState,
169 config: TutorialScreenConfig,
170 isCompactWindow: Boolean,
171 modifier: Modifier = Modifier,
172 ) {
173 val horizontalPadding = if (isCompactWindow) 24.dp else 96.dp
174 // Represents the majority of tablets in portrait - we need extra spacer at the top and bottom
175 val isTablet = LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Expanded
176 Column(
177 modifier =
178 modifier.fillMaxWidth().padding(start = 0.dp, top = 100.dp, end = 0.dp, bottom = 8.dp)
179 ) {
180 if (isTablet) Spacer(modifier = Modifier.weight(0.3f))
181 TutorialDescription(
182 actionState,
183 config,
184 isCompactWindow,
185 modifier = Modifier.weight(1f).padding(horizontal = horizontalPadding),
186 )
187 TutorialAnimation(actionState, config, modifier = Modifier.weight(1.8f).fillMaxWidth())
188 if (isTablet) Spacer(modifier = Modifier.weight(0.3f))
189 }
190 }
191
192 @Composable
TutorialDescriptionnull193 fun TutorialDescription(
194 actionState: TutorialActionState,
195 config: TutorialScreenConfig,
196 isCompactWindow: Boolean,
197 modifier: Modifier = Modifier,
198 ) {
199 val focusRequester = remember { FocusRequester() }
200 LaunchedEffect(Unit) { focusRequester.requestFocus() }
201 val (titleTextId, bodyTextId) =
202 when (actionState) {
203 is Finished -> config.strings.titleSuccessResId to config.strings.bodySuccessResId
204 Error,
205 is InProgressAfterError ->
206 config.strings.titleErrorResId to config.strings.bodyErrorResId
207 is NotStarted,
208 is InProgress -> config.strings.titleResId to config.strings.bodyResId
209 }
210 Column(verticalArrangement = Arrangement.Top, modifier = modifier) {
211 Text(
212 text = stringResource(id = titleTextId),
213 style =
214 if (isCompactWindow) MaterialTheme.typography.headlineLarge
215 else MaterialTheme.typography.displayMedium,
216 color = config.colors.title,
217 modifier = Modifier.focusRequester(focusRequester).focusable(),
218 )
219 Spacer(modifier = Modifier.height(16.dp))
220 Text(
221 text = stringResource(id = bodyTextId),
222 style = MaterialTheme.typography.bodyLarge,
223 color = config.colors.bodyText,
224 )
225 }
226 }
227