<lambda>null1 package com.android.systemui.communal.ui.compose
2
3 import android.content.res.Configuration
4 import androidx.compose.animation.core.CubicBezierEasing
5 import androidx.compose.animation.core.RepeatMode
6 import androidx.compose.animation.core.animateFloat
7 import androidx.compose.animation.core.infiniteRepeatable
8 import androidx.compose.animation.core.rememberInfiniteTransition
9 import androidx.compose.animation.core.tween
10 import androidx.compose.foundation.background
11 import androidx.compose.foundation.focusable
12 import androidx.compose.foundation.isSystemInDarkTheme
13 import androidx.compose.foundation.layout.Box
14 import androidx.compose.foundation.layout.BoxScope
15 import androidx.compose.foundation.layout.fillMaxSize
16 import androidx.compose.material3.MaterialTheme
17 import androidx.compose.runtime.Composable
18 import androidx.compose.runtime.DisposableEffect
19 import androidx.compose.runtime.LaunchedEffect
20 import androidx.compose.runtime.getValue
21 import androidx.compose.runtime.remember
22 import androidx.compose.runtime.rememberCoroutineScope
23 import androidx.compose.ui.Modifier
24 import androidx.compose.ui.draw.alpha
25 import androidx.compose.ui.draw.blur
26 import androidx.compose.ui.draw.drawBehind
27 import androidx.compose.ui.geometry.Offset
28 import androidx.compose.ui.graphics.BlendMode
29 import androidx.compose.ui.graphics.Brush
30 import androidx.compose.ui.graphics.Color
31 import androidx.compose.ui.platform.LocalConfiguration
32 import androidx.compose.ui.platform.LocalDensity
33 import androidx.compose.ui.semantics.clearAndSetSemantics
34 import androidx.compose.ui.semantics.disabled
35 import androidx.compose.ui.semantics.semantics
36 import androidx.compose.ui.unit.dp
37 import androidx.lifecycle.compose.collectAsStateWithLifecycle
38 import com.android.compose.animation.scene.ContentKey
39 import com.android.compose.animation.scene.ContentScope
40 import com.android.compose.animation.scene.Edge
41 import com.android.compose.animation.scene.ElementKey
42 import com.android.compose.animation.scene.ElementMatcher
43 import com.android.compose.animation.scene.LowestZIndexContentPicker
44 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
45 import com.android.compose.animation.scene.SceneKey
46 import com.android.compose.animation.scene.SceneTransitionLayout
47 import com.android.compose.animation.scene.Swipe
48 import com.android.compose.animation.scene.UserActionResult
49 import com.android.compose.animation.scene.observableTransitionState
50 import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState
51 import com.android.compose.animation.scene.transitions
52 import com.android.compose.modifiers.thenIf
53 import com.android.systemui.Flags
54 import com.android.systemui.communal.shared.model.CommunalBackgroundType
55 import com.android.systemui.communal.shared.model.CommunalScenes
56 import com.android.systemui.communal.shared.model.CommunalTransitionKeys
57 import com.android.systemui.communal.ui.compose.Dimensions.Companion.SlideOffsetY
58 import com.android.systemui.communal.ui.compose.extensions.allowGestures
59 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
60 import com.android.systemui.communal.util.CommunalColors
61 import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
62 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor.Companion.TO_GONE_DURATION
63 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
64 import com.android.systemui.scene.ui.composable.SceneTransitionLayoutDataSource
65 import kotlin.time.DurationUnit
66
67 object Communal {
68 object Elements {
69 val Scrim = ElementKey("Scrim", contentPicker = LowestZIndexContentPicker)
70 val Grid = ElementKey("CommunalContent")
71 val LockIcon = ElementKey("CommunalLockIcon")
72 val IndicationArea = ElementKey("CommunalIndicationArea")
73 val StatusBar = ElementKey("StatusBar")
74 }
75 }
76
77 object AllElements : ElementMatcher {
matchesnull78 override fun matches(key: ElementKey, content: ContentKey) = true
79 }
80
81 object TransitionDuration {
82 const val BETWEEN_HUB_AND_EDIT_MODE_MS = 1000
83 const val EDIT_MODE_TO_HUB_CONTENT_MS = 167
84 const val EDIT_MODE_TO_HUB_GRID_DELAY_MS = 167
85 const val EDIT_MODE_TO_HUB_GRID_END_MS =
86 EDIT_MODE_TO_HUB_GRID_DELAY_MS + EDIT_MODE_TO_HUB_CONTENT_MS
87 const val HUB_TO_EDIT_MODE_CONTENT_MS = 250
88 const val TO_GLANCEABLE_HUB_DURATION_MS = 1000
89 }
90
<lambda>null91 val sceneTransitionsV2 = transitions {
92 to(CommunalScenes.Communal) {
93 spec = tween(durationMillis = TransitionDuration.TO_GLANCEABLE_HUB_DURATION_MS)
94 fade(AllElements)
95 }
96 to(CommunalScenes.Communal, key = CommunalTransitionKeys.Swipe) {
97 spec = tween(durationMillis = TransitionDuration.TO_GLANCEABLE_HUB_DURATION_MS)
98 translate(Communal.Elements.Grid, Edge.End)
99 timestampRange(startMillis = 167, endMillis = 334) { fade(AllElements) }
100 }
101 to(CommunalScenes.Blank) {
102 spec = tween(durationMillis = TO_GONE_DURATION.toInt(DurationUnit.MILLISECONDS))
103 fade(AllElements)
104 }
105 to(CommunalScenes.Blank, key = CommunalTransitionKeys.SwipeInLandscape) {
106 spec = tween(durationMillis = TO_LOCKSCREEN_DURATION.toInt(DurationUnit.MILLISECONDS))
107 translate(Communal.Elements.Grid, Edge.End)
108 timestampRange(endMillis = 167) {
109 fade(Communal.Elements.Grid)
110 fade(Communal.Elements.IndicationArea)
111 fade(Communal.Elements.LockIcon)
112 fade(Communal.Elements.StatusBar)
113 }
114 timestampRange(startMillis = 167, endMillis = 500) { fade(Communal.Elements.Scrim) }
115 }
116 to(CommunalScenes.Blank, key = CommunalTransitionKeys.Swipe) {
117 spec = tween(durationMillis = TransitionDuration.TO_GLANCEABLE_HUB_DURATION_MS)
118 translate(Communal.Elements.Grid, Edge.End)
119 timestampRange(endMillis = 167) {
120 fade(Communal.Elements.Grid)
121 fade(Communal.Elements.IndicationArea)
122 fade(Communal.Elements.LockIcon)
123 if (!Flags.glanceableHubV2()) {
124 fade(Communal.Elements.StatusBar)
125 }
126 }
127 timestampRange(startMillis = 167, endMillis = 334) { fade(Communal.Elements.Scrim) }
128 }
129 }
130
<lambda>null131 val sceneTransitions = transitions {
132 to(CommunalScenes.Communal, key = CommunalTransitionKeys.SimpleFade) {
133 spec = tween(durationMillis = 250)
134 fade(AllElements)
135 }
136 to(CommunalScenes.Blank, key = CommunalTransitionKeys.SimpleFade) {
137 spec = tween(durationMillis = TO_GONE_DURATION.toInt(DurationUnit.MILLISECONDS))
138 fade(AllElements)
139 }
140 to(CommunalScenes.Communal) {
141 spec = tween(durationMillis = 1000)
142 translate(Communal.Elements.Grid, Edge.End)
143 timestampRange(startMillis = 167, endMillis = 334) { fade(AllElements) }
144 }
145 to(CommunalScenes.Blank) {
146 spec = tween(durationMillis = 1000)
147 translate(Communal.Elements.Grid, Edge.End)
148 timestampRange(endMillis = 167) {
149 fade(Communal.Elements.Grid)
150 fade(Communal.Elements.IndicationArea)
151 fade(Communal.Elements.LockIcon)
152 if (!Flags.glanceableHubV2()) {
153 fade(Communal.Elements.StatusBar)
154 }
155 }
156 timestampRange(startMillis = 167, endMillis = 334) { fade(Communal.Elements.Scrim) }
157 }
158 to(CommunalScenes.Blank, key = CommunalTransitionKeys.ToEditMode) {
159 spec = tween(durationMillis = TransitionDuration.BETWEEN_HUB_AND_EDIT_MODE_MS)
160 timestampRange(endMillis = TransitionDuration.HUB_TO_EDIT_MODE_CONTENT_MS) {
161 fade(Communal.Elements.Grid)
162 fade(Communal.Elements.IndicationArea)
163 fade(Communal.Elements.LockIcon)
164 }
165 fade(Communal.Elements.Scrim)
166 }
167 to(CommunalScenes.Communal, key = CommunalTransitionKeys.FromEditMode) {
168 spec = tween(durationMillis = TransitionDuration.BETWEEN_HUB_AND_EDIT_MODE_MS)
169 translate(Communal.Elements.Grid, y = SlideOffsetY)
170 timestampRange(endMillis = TransitionDuration.EDIT_MODE_TO_HUB_CONTENT_MS) {
171 fade(Communal.Elements.IndicationArea)
172 fade(Communal.Elements.LockIcon)
173 fade(Communal.Elements.Scrim)
174 }
175 timestampRange(
176 startMillis = TransitionDuration.EDIT_MODE_TO_HUB_GRID_DELAY_MS,
177 endMillis = TransitionDuration.EDIT_MODE_TO_HUB_GRID_END_MS,
178 ) {
179 fade(Communal.Elements.Grid)
180 }
181 }
182 }
183
184 /**
185 * View containing a [SceneTransitionLayout] that shows the communal UI and handles transitions.
186 *
187 * This is a temporary container to allow the communal UI to use [SceneTransitionLayout] for gesture
188 * handling and transitions before the full Flexiglass layout is ready.
189 */
190 @Composable
CommunalContainernull191 fun CommunalContainer(
192 modifier: Modifier = Modifier,
193 viewModel: CommunalViewModel,
194 dataSourceDelegator: SceneDataSourceDelegator,
195 colors: CommunalColors,
196 content: CommunalContent,
197 ) {
198 val coroutineScope = rememberCoroutineScope()
199 val currentSceneKey: SceneKey by viewModel.currentScene.collectAsStateWithLifecycle()
200 val touchesAllowed by viewModel.touchesAllowed.collectAsStateWithLifecycle()
201 val backgroundType by
202 viewModel.communalBackground.collectAsStateWithLifecycle(
203 initialValue = CommunalBackgroundType.ANIMATED
204 )
205 val swipeToHubEnabled by viewModel.swipeToHubEnabled.collectAsStateWithLifecycle(false)
206 val state: MutableSceneTransitionLayoutState =
207 rememberMutableSceneTransitionLayoutState(
208 initialScene = currentSceneKey,
209 canChangeScene = { _ -> viewModel.canChangeScene() },
210 transitions = if (viewModel.v2FlagEnabled()) sceneTransitionsV2 else sceneTransitions,
211 )
212
213 val isUiBlurred by viewModel.isUiBlurred.collectAsStateWithLifecycle()
214
215 val detector = remember { CommunalSwipeDetector() }
216
217 DisposableEffect(state) {
218 val dataSource = SceneTransitionLayoutDataSource(state, coroutineScope)
219 dataSourceDelegator.setDelegate(dataSource)
220 onDispose { dataSourceDelegator.setDelegate(null) }
221 }
222
223 // This effect exposes the SceneTransitionLayout's observable transition state to the rest of
224 // the system, and unsets it when the view is disposed to avoid a memory leak.
225 DisposableEffect(viewModel, state) {
226 viewModel.setTransitionState(state.observableTransitionState())
227 onDispose { viewModel.setTransitionState(null) }
228 }
229
230 val blurRadius = with(LocalDensity.current) { viewModel.blurRadiusPx.toDp() }
231
232 val swipeFromHubInLandscape by
233 viewModel.swipeFromHubInLandscape.collectAsStateWithLifecycle(false)
234
235 SceneTransitionLayout(
236 state = state,
237 modifier = modifier.fillMaxSize().thenIf(isUiBlurred) { Modifier.blur(blurRadius) },
238 swipeSourceDetector = detector,
239 swipeDetector = detector,
240 ) {
241 scene(
242 CommunalScenes.Blank,
243 userActions =
244 if (swipeToHubEnabled) {
245 mapOf(
246 Swipe.Start(fromSource = Edge.End) to
247 UserActionResult(CommunalScenes.Communal, CommunalTransitionKeys.Swipe)
248 )
249 } else {
250 emptyMap()
251 },
252 ) {
253 // This scene shows nothing only allowing for transitions to the communal scene.
254 Box(modifier = Modifier.fillMaxSize())
255 }
256
257 scene(
258 CommunalScenes.Communal,
259 userActions =
260 mapOf(
261 Swipe.End to
262 UserActionResult(
263 CommunalScenes.Blank,
264 if (swipeFromHubInLandscape) {
265 CommunalTransitionKeys.SwipeInLandscape
266 } else {
267 CommunalTransitionKeys.Swipe
268 },
269 )
270 ),
271 ) {
272 CommunalScene(
273 backgroundType = backgroundType,
274 colors = colors,
275 content = content,
276 viewModel = viewModel,
277 )
278 }
279 }
280
281 // Touches on the notification shade in blank areas fall through to the glanceable hub. When the
282 // shade is showing, we block all touches in order to prevent this unwanted behavior.
283 Box(modifier = Modifier.fillMaxSize().allowGestures(touchesAllowed))
284 }
285
286 /** Listens to orientation changes on communal scene and reset when scene is disposed. */
287 @Composable
ObserveOrientationChangenull288 fun ObserveOrientationChange(viewModel: CommunalViewModel) {
289 val configuration = LocalConfiguration.current
290
291 LaunchedEffect(configuration.orientation) {
292 viewModel.onOrientationChange(configuration.orientation)
293 }
294
295 DisposableEffect(Unit) {
296 onDispose { viewModel.onOrientationChange(Configuration.ORIENTATION_UNDEFINED) }
297 }
298 }
299
300 /** Scene containing the glanceable hub UI. */
301 @Composable
ContentScopenull302 fun ContentScope.CommunalScene(
303 backgroundType: CommunalBackgroundType,
304 colors: CommunalColors,
305 content: CommunalContent,
306 viewModel: CommunalViewModel,
307 modifier: Modifier = Modifier,
308 ) {
309 val isFocusable by viewModel.isFocusable.collectAsStateWithLifecycle(initialValue = false)
310
311 // Observe screen rotation while Communal Scene is active.
312 ObserveOrientationChange(viewModel)
313 Box(
314 modifier =
315 Modifier.element(Communal.Elements.Scrim)
316 .fillMaxSize()
317 .then(
318 if (isFocusable) {
319 Modifier.focusable()
320 } else {
321 Modifier.semantics { disabled() }.clearAndSetSemantics {}
322 }
323 )
324 ) {
325 when (backgroundType) {
326 CommunalBackgroundType.STATIC -> DefaultBackground(colors = colors)
327 CommunalBackgroundType.STATIC_GRADIENT -> StaticLinearGradient()
328 CommunalBackgroundType.ANIMATED -> AnimatedLinearGradient()
329 CommunalBackgroundType.NONE -> BackgroundTopScrim()
330 CommunalBackgroundType.BLUR -> Background()
331 CommunalBackgroundType.SCRIM -> Scrimmed()
332 }
333
334 with(content) {
335 Content(
336 modifier =
337 modifier.focusable(isFocusable).semantics {
338 if (!isFocusable) {
339 disabled()
340 }
341 }
342 )
343 }
344 }
345 }
346
347 /** Default background of the hub, a single color */
348 @Composable
BoxScopenull349 private fun BoxScope.DefaultBackground(colors: CommunalColors) {
350 val backgroundColor by colors.backgroundColor.collectAsStateWithLifecycle()
351 Box(modifier = Modifier.matchParentSize().background(Color(backgroundColor.toArgb())))
352 }
353
354 @Composable
Scrimmednull355 private fun BoxScope.Scrimmed() {
356 Box(modifier = Modifier.matchParentSize().alpha(0.34f).background(Color.Black))
357 }
358
359 /** Experimental hub background, static linear gradient */
360 @Composable
StaticLinearGradientnull361 private fun BoxScope.StaticLinearGradient() {
362 val colors = MaterialTheme.colorScheme
363 Box(
364 Modifier.matchParentSize()
365 .background(
366 Brush.linearGradient(colors = listOf(colors.primary, colors.primaryContainer))
367 )
368 )
369 BackgroundTopScrim()
370 }
371
372 /** Experimental hub background, animated linear gradient */
373 @Composable
AnimatedLinearGradientnull374 private fun BoxScope.AnimatedLinearGradient() {
375 val colors = MaterialTheme.colorScheme
376 Box(
377 Modifier.matchParentSize()
378 .background(colors.primary)
379 .animatedRadialGradientBackground(
380 toColor = colors.primary,
381 fromColor = colors.primaryContainer.copy(alpha = 0.6f),
382 )
383 )
384 BackgroundTopScrim()
385 }
386
387 /** Scrim placed on top of the background in order to dim/bright colors */
388 @Composable
BackgroundTopScrimnull389 private fun BoxScope.BackgroundTopScrim() {
390 val darkTheme = isSystemInDarkTheme()
391 val scrimOnTopColor = if (darkTheme) Color.Black else Color.White
392 Box(Modifier.matchParentSize().alpha(0.34f).background(scrimOnTopColor))
393 }
394
395 /** Transparent (nothing) composable for when the background is blurred. */
Backgroundnull396 @Composable private fun BoxScope.Background() {}
397
398 /** The duration to use for the gradient background animation. */
399 private const val ANIMATION_DURATION_MS = 10_000
400
401 /** The offset to use in order to place the center of each gradient offscreen. */
402 private val ANIMATION_OFFSCREEN_OFFSET = 128.dp
403
404 /** Modifier which creates two radial gradients that animate up and down. */
405 @Composable
animatedRadialGradientBackgroundnull406 fun Modifier.animatedRadialGradientBackground(toColor: Color, fromColor: Color): Modifier {
407 val density = LocalDensity.current
408 val infiniteTransition = rememberInfiniteTransition(label = "radial gradient transition")
409 val centerFraction by
410 infiniteTransition.animateFloat(
411 initialValue = 0f,
412 targetValue = 1f,
413 animationSpec =
414 infiniteRepeatable(
415 animation =
416 tween(
417 durationMillis = ANIMATION_DURATION_MS,
418 easing = CubicBezierEasing(0.33f, 0f, 0.67f, 1f),
419 ),
420 repeatMode = RepeatMode.Reverse,
421 ),
422 label = "radial gradient center fraction",
423 )
424
425 // Offset to place the center of the gradients offscreen. This is applied to both the
426 // x and y coordinates.
427 val offsetPx = remember(density) { with(density) { ANIMATION_OFFSCREEN_OFFSET.toPx() } }
428
429 return drawBehind {
430 val gradientRadius = (size.width / 2) + offsetPx
431 val totalHeight = size.height + 2 * offsetPx
432
433 val leftCenter = Offset(x = -offsetPx, y = totalHeight * centerFraction - offsetPx)
434 val rightCenter =
435 Offset(x = offsetPx + size.width, y = totalHeight * (1f - centerFraction) - offsetPx)
436
437 // Right gradient
438 drawCircle(
439 brush =
440 Brush.radialGradient(
441 colors = listOf(fromColor, toColor),
442 center = rightCenter,
443 radius = gradientRadius,
444 ),
445 center = rightCenter,
446 radius = gradientRadius,
447 blendMode = BlendMode.SrcAtop,
448 )
449
450 // Left gradient
451 drawCircle(
452 brush =
453 Brush.radialGradient(
454 colors = listOf(fromColor, toColor),
455 center = leftCenter,
456 radius = gradientRadius,
457 ),
458 center = leftCenter,
459 radius = gradientRadius,
460 blendMode = BlendMode.SrcAtop,
461 )
462 }
463 }
464