<lambda>null1 package com.android.systemui.communal.ui.compose
2
3 import androidx.compose.animation.core.CubicBezierEasing
4 import androidx.compose.animation.core.RepeatMode
5 import androidx.compose.animation.core.animateFloat
6 import androidx.compose.animation.core.infiniteRepeatable
7 import androidx.compose.animation.core.rememberInfiniteTransition
8 import androidx.compose.animation.core.tween
9 import androidx.compose.foundation.background
10 import androidx.compose.foundation.isSystemInDarkTheme
11 import androidx.compose.foundation.layout.Arrangement
12 import androidx.compose.foundation.layout.Box
13 import androidx.compose.foundation.layout.BoxScope
14 import androidx.compose.foundation.layout.Row
15 import androidx.compose.foundation.layout.Spacer
16 import androidx.compose.foundation.layout.fillMaxSize
17 import androidx.compose.foundation.layout.height
18 import androidx.compose.foundation.layout.width
19 import androidx.compose.foundation.shape.RoundedCornerShape
20 import androidx.compose.runtime.Composable
21 import androidx.compose.runtime.DisposableEffect
22 import androidx.compose.runtime.getValue
23 import androidx.compose.runtime.remember
24 import androidx.compose.runtime.rememberCoroutineScope
25 import androidx.compose.ui.Alignment
26 import androidx.compose.ui.Modifier
27 import androidx.compose.ui.draw.alpha
28 import androidx.compose.ui.draw.drawBehind
29 import androidx.compose.ui.geometry.Offset
30 import androidx.compose.ui.graphics.BlendMode
31 import androidx.compose.ui.graphics.Brush
32 import androidx.compose.ui.graphics.Color
33 import androidx.compose.ui.platform.LocalDensity
34 import androidx.compose.ui.res.dimensionResource
35 import androidx.compose.ui.unit.dp
36 import androidx.lifecycle.compose.collectAsStateWithLifecycle
37 import com.android.compose.animation.scene.CommunalSwipeDetector
38 import com.android.compose.animation.scene.DefaultSwipeDetector
39 import com.android.compose.animation.scene.Edge
40 import com.android.compose.animation.scene.ElementKey
41 import com.android.compose.animation.scene.ElementMatcher
42 import com.android.compose.animation.scene.FixedSizeEdgeDetector
43 import com.android.compose.animation.scene.LowestZIndexScenePicker
44 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
45 import com.android.compose.animation.scene.SceneKey
46 import com.android.compose.animation.scene.SceneScope
47 import com.android.compose.animation.scene.SceneTransitionLayout
48 import com.android.compose.animation.scene.Swipe
49 import com.android.compose.animation.scene.SwipeDirection
50 import com.android.compose.animation.scene.observableTransitionState
51 import com.android.compose.animation.scene.transitions
52 import com.android.compose.theme.LocalAndroidColorScheme
53 import com.android.systemui.Flags
54 import com.android.systemui.Flags.glanceableHubFullscreenSwipe
55 import com.android.systemui.communal.shared.model.CommunalBackgroundType
56 import com.android.systemui.communal.shared.model.CommunalScenes
57 import com.android.systemui.communal.shared.model.CommunalTransitionKeys
58 import com.android.systemui.communal.ui.compose.Dimensions.SlideOffsetY
59 import com.android.systemui.communal.ui.compose.extensions.allowGestures
60 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
61 import com.android.systemui.communal.util.CommunalColors
62 import com.android.systemui.res.R
63 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
64 import com.android.systemui.scene.ui.composable.SceneTransitionLayoutDataSource
65
66 object Communal {
67 object Elements {
68 val Scrim = ElementKey("Scrim", scenePicker = LowestZIndexScenePicker)
69 val Grid = ElementKey("CommunalContent")
70 val LockIcon = ElementKey("CommunalLockIcon")
71 val IndicationArea = ElementKey("CommunalIndicationArea")
72 val StatusBar = ElementKey("StatusBar")
73 }
74 }
75
76 object AllElements : ElementMatcher {
matchesnull77 override fun matches(key: ElementKey, scene: SceneKey) = true
78 }
79
80 private object TransitionDuration {
81 const val BETWEEN_HUB_AND_EDIT_MODE_MS = 1000
82 const val EDIT_MODE_TO_HUB_CONTENT_MS = 167
83 const val EDIT_MODE_TO_HUB_GRID_DELAY_MS = 167
84 const val EDIT_MODE_TO_HUB_GRID_END_MS =
85 EDIT_MODE_TO_HUB_GRID_DELAY_MS + EDIT_MODE_TO_HUB_CONTENT_MS
86 const val HUB_TO_EDIT_MODE_CONTENT_MS = 250
87 }
88
<lambda>null89 val sceneTransitions = transitions {
90 to(CommunalScenes.Communal, key = CommunalTransitionKeys.SimpleFade) {
91 spec = tween(durationMillis = 250)
92 fade(AllElements)
93 }
94 to(CommunalScenes.Communal) {
95 spec = tween(durationMillis = 1000)
96 translate(Communal.Elements.Grid, Edge.Right)
97 timestampRange(startMillis = 167, endMillis = 334) { fade(AllElements) }
98 }
99 to(CommunalScenes.Blank) {
100 spec = tween(durationMillis = 1000)
101 translate(Communal.Elements.Grid, Edge.Right)
102 timestampRange(endMillis = 167) {
103 fade(Communal.Elements.Grid)
104 fade(Communal.Elements.IndicationArea)
105 fade(Communal.Elements.LockIcon)
106 fade(Communal.Elements.StatusBar)
107 }
108 timestampRange(startMillis = 167, endMillis = 334) { fade(Communal.Elements.Scrim) }
109 }
110 to(CommunalScenes.Blank, key = CommunalTransitionKeys.ToEditMode) {
111 spec = tween(durationMillis = TransitionDuration.BETWEEN_HUB_AND_EDIT_MODE_MS)
112 timestampRange(endMillis = TransitionDuration.HUB_TO_EDIT_MODE_CONTENT_MS) {
113 fade(Communal.Elements.Grid)
114 fade(Communal.Elements.IndicationArea)
115 fade(Communal.Elements.LockIcon)
116 }
117 fade(Communal.Elements.Scrim)
118 }
119 to(CommunalScenes.Communal, key = CommunalTransitionKeys.FromEditMode) {
120 spec = tween(durationMillis = TransitionDuration.BETWEEN_HUB_AND_EDIT_MODE_MS)
121 translate(Communal.Elements.Grid, y = SlideOffsetY)
122 timestampRange(endMillis = TransitionDuration.EDIT_MODE_TO_HUB_CONTENT_MS) {
123 fade(Communal.Elements.IndicationArea)
124 fade(Communal.Elements.LockIcon)
125 fade(Communal.Elements.Scrim)
126 }
127 timestampRange(
128 startMillis = TransitionDuration.EDIT_MODE_TO_HUB_GRID_DELAY_MS,
129 endMillis = TransitionDuration.EDIT_MODE_TO_HUB_GRID_END_MS
130 ) {
131 fade(Communal.Elements.Grid)
132 }
133 }
134 }
135
136 /**
137 * View containing a [SceneTransitionLayout] that shows the communal UI and handles transitions.
138 *
139 * This is a temporary container to allow the communal UI to use [SceneTransitionLayout] for gesture
140 * handling and transitions before the full Flexiglass layout is ready.
141 */
142 @Composable
CommunalContainernull143 fun CommunalContainer(
144 modifier: Modifier = Modifier,
145 viewModel: CommunalViewModel,
146 dataSourceDelegator: SceneDataSourceDelegator,
147 colors: CommunalColors,
148 content: CommunalContent,
149 ) {
150 val coroutineScope = rememberCoroutineScope()
151 val currentSceneKey: SceneKey by
152 viewModel.currentScene.collectAsStateWithLifecycle(CommunalScenes.Blank)
153 val touchesAllowed by viewModel.touchesAllowed.collectAsStateWithLifecycle(initialValue = false)
154 val showGestureIndicator by
155 viewModel.showGestureIndicator.collectAsStateWithLifecycle(initialValue = false)
156 val backgroundType by
157 viewModel.communalBackground.collectAsStateWithLifecycle(
158 initialValue = CommunalBackgroundType.DEFAULT
159 )
160 val state: MutableSceneTransitionLayoutState = remember {
161 MutableSceneTransitionLayoutState(
162 initialScene = currentSceneKey,
163 canChangeScene = { _ -> viewModel.canChangeScene() },
164 transitions = sceneTransitions,
165 enableInterruptions = false,
166 )
167 }
168
169 val detector = remember { CommunalSwipeDetector() }
170
171 DisposableEffect(state) {
172 val dataSource = SceneTransitionLayoutDataSource(state, coroutineScope)
173 dataSourceDelegator.setDelegate(dataSource)
174 onDispose { dataSourceDelegator.setDelegate(null) }
175 }
176
177 // This effect exposes the SceneTransitionLayout's observable transition state to the rest of
178 // the system, and unsets it when the view is disposed to avoid a memory leak.
179 DisposableEffect(viewModel, state) {
180 viewModel.setTransitionState(state.observableTransitionState())
181 onDispose { viewModel.setTransitionState(null) }
182 }
183
184 val swipeSourceDetector =
185 if (glanceableHubFullscreenSwipe()) {
186 detector
187 } else {
188 FixedSizeEdgeDetector(dimensionResource(id = R.dimen.communal_gesture_initiation_width))
189 }
190
191 val swipeDetector =
192 if (glanceableHubFullscreenSwipe()) {
193 detector
194 } else {
195 DefaultSwipeDetector
196 }
197
198 SceneTransitionLayout(
199 state = state,
200 modifier = modifier.fillMaxSize(),
201 swipeSourceDetector = swipeSourceDetector,
202 swipeDetector = swipeDetector,
203 ) {
204 scene(
205 CommunalScenes.Blank,
206 userActions =
207 mapOf(
208 Swipe(SwipeDirection.Left, fromSource = Edge.Right) to CommunalScenes.Communal
209 )
210 ) {
211 // This scene shows nothing only allowing for transitions to the communal scene.
212 // TODO(b/339667383): remove this temporary swipe gesture handle
213 Row(modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.End) {
214 if (showGestureIndicator && Flags.glanceableHubGestureHandle()) {
215 Box(
216 modifier =
217 Modifier.height(220.dp)
218 .width(4.dp)
219 .align(Alignment.CenterVertically)
220 .background(color = Color.White, RoundedCornerShape(4.dp))
221 )
222 Spacer(modifier = Modifier.width(12.dp))
223 }
224 }
225 }
226
227 scene(
228 CommunalScenes.Communal,
229 userActions =
230 mapOf(Swipe(SwipeDirection.Right, fromSource = Edge.Left) to CommunalScenes.Blank)
231 ) {
232 CommunalScene(backgroundType, colors, content)
233 }
234 }
235
236 // Touches on the notification shade in blank areas fall through to the glanceable hub. When the
237 // shade is showing, we block all touches in order to prevent this unwanted behavior.
238 Box(modifier = Modifier.fillMaxSize().allowGestures(touchesAllowed))
239 }
240
241 /** Scene containing the glanceable hub UI. */
242 @Composable
CommunalScenenull243 private fun SceneScope.CommunalScene(
244 backgroundType: CommunalBackgroundType,
245 colors: CommunalColors,
246 content: CommunalContent,
247 modifier: Modifier = Modifier,
248 ) {
249 Box(modifier = Modifier.element(Communal.Elements.Scrim).fillMaxSize()) {
250 when (backgroundType) {
251 CommunalBackgroundType.DEFAULT -> DefaultBackground(colors = colors)
252 CommunalBackgroundType.STATIC_GRADIENT -> StaticLinearGradient()
253 CommunalBackgroundType.ANIMATED -> AnimatedLinearGradient()
254 CommunalBackgroundType.NONE -> BackgroundTopScrim()
255 }
256 }
257 with(content) { Content(modifier = modifier) }
258 }
259
260 /** Default background of the hub, a single color */
261 @Composable
BoxScopenull262 private fun BoxScope.DefaultBackground(
263 colors: CommunalColors,
264 ) {
265 val backgroundColor by colors.backgroundColor.collectAsStateWithLifecycle()
266 Box(
267 modifier = Modifier.matchParentSize().background(Color(backgroundColor.toArgb())),
268 )
269 }
270
271 /** Experimental hub background, static linear gradient */
272 @Composable
StaticLinearGradientnull273 private fun BoxScope.StaticLinearGradient() {
274 val colors = LocalAndroidColorScheme.current
275 Box(
276 Modifier.matchParentSize()
277 .background(
278 Brush.linearGradient(colors = listOf(colors.primary, colors.primaryContainer)),
279 )
280 )
281 BackgroundTopScrim()
282 }
283
284 /** Experimental hub background, animated linear gradient */
285 @Composable
AnimatedLinearGradientnull286 private fun BoxScope.AnimatedLinearGradient() {
287 val colors = LocalAndroidColorScheme.current
288 Box(
289 Modifier.matchParentSize()
290 .background(colors.primary)
291 .animatedRadialGradientBackground(
292 toColor = colors.primary,
293 fromColor = colors.primaryContainer.copy(alpha = 0.6f)
294 )
295 )
296 BackgroundTopScrim()
297 }
298
299 /** Scrim placed on top of the background in order to dim/bright colors */
300 @Composable
BackgroundTopScrimnull301 private fun BoxScope.BackgroundTopScrim() {
302 val darkTheme = isSystemInDarkTheme()
303 val scrimOnTopColor = if (darkTheme) Color.Black else Color.White
304 Box(Modifier.matchParentSize().alpha(0.34f).background(scrimOnTopColor))
305 }
306
307 /** The duration to use for the gradient background animation. */
308 private const val ANIMATION_DURATION_MS = 10_000
309
310 /** The offset to use in order to place the center of each gradient offscreen. */
311 private val ANIMATION_OFFSCREEN_OFFSET = 128.dp
312
313 /** Modifier which creates two radial gradients that animate up and down. */
314 @Composable
animatedRadialGradientBackgroundnull315 fun Modifier.animatedRadialGradientBackground(toColor: Color, fromColor: Color): Modifier {
316 val density = LocalDensity.current
317 val infiniteTransition = rememberInfiniteTransition(label = "radial gradient transition")
318 val centerFraction by
319 infiniteTransition.animateFloat(
320 initialValue = 0f,
321 targetValue = 1f,
322 animationSpec =
323 infiniteRepeatable(
324 animation =
325 tween(
326 durationMillis = ANIMATION_DURATION_MS,
327 easing = CubicBezierEasing(0.33f, 0f, 0.67f, 1f),
328 ),
329 repeatMode = RepeatMode.Reverse
330 ),
331 label = "radial gradient center fraction"
332 )
333
334 // Offset to place the center of the gradients offscreen. This is applied to both the
335 // x and y coordinates.
336 val offsetPx = remember(density) { with(density) { ANIMATION_OFFSCREEN_OFFSET.toPx() } }
337
338 return drawBehind {
339 val gradientRadius = (size.width / 2) + offsetPx
340 val totalHeight = size.height + 2 * offsetPx
341
342 val leftCenter =
343 Offset(
344 x = -offsetPx,
345 y = totalHeight * centerFraction - offsetPx,
346 )
347 val rightCenter =
348 Offset(
349 x = offsetPx + size.width,
350 y = totalHeight * (1f - centerFraction) - offsetPx,
351 )
352
353 // Right gradient
354 drawCircle(
355 brush =
356 Brush.radialGradient(
357 colors = listOf(fromColor, toColor),
358 center = rightCenter,
359 radius = gradientRadius
360 ),
361 center = rightCenter,
362 radius = gradientRadius,
363 blendMode = BlendMode.SrcAtop,
364 )
365
366 // Left gradient
367 drawCircle(
368 brush =
369 Brush.radialGradient(
370 colors = listOf(fromColor, toColor),
371 center = leftCenter,
372 radius = gradientRadius
373 ),
374 center = leftCenter,
375 radius = gradientRadius,
376 blendMode = BlendMode.SrcAtop,
377 )
378 }
379 }
380