• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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