• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<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