<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