• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright 2023 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.compose.animation.scene.demo.notification
18 
19 import androidx.compose.foundation.OverscrollEffect
20 import androidx.compose.foundation.layout.Arrangement
21 import androidx.compose.foundation.layout.Column
22 import androidx.compose.foundation.layout.fillMaxWidth
23 import androidx.compose.foundation.layout.padding
24 import androidx.compose.foundation.rememberScrollState
25 import androidx.compose.foundation.verticalScroll
26 import androidx.compose.foundation.withoutVisualEffect
27 import androidx.compose.runtime.Composable
28 import androidx.compose.runtime.LaunchedEffect
29 import androidx.compose.runtime.key
30 import androidx.compose.runtime.rememberCoroutineScope
31 import androidx.compose.ui.Modifier
32 import androidx.compose.ui.unit.dp
33 import androidx.compose.ui.zIndex
34 import com.android.compose.animation.scene.ContentScope
35 import com.android.compose.animation.scene.ElementKey
36 import com.android.compose.animation.scene.ObservableTransitionState
37 import com.android.compose.animation.scene.SceneTransitionLayoutState
38 import com.android.compose.animation.scene.demo.DemoConfiguration
39 import com.android.compose.animation.scene.demo.Scenes
40 import com.android.compose.animation.scene.observableTransitionState
41 import com.android.compose.gesture.NestedScrollableBound
42 import com.android.compose.modifiers.thenIf
43 import kotlinx.coroutines.CoroutineScope
44 import kotlinx.coroutines.flow.combine
45 import kotlinx.coroutines.flow.filter
46 import kotlinx.coroutines.flow.filterIsInstance
47 import kotlinx.coroutines.flow.first
48 import kotlinx.coroutines.flow.flatMapLatest
49 
50 class NotificationIdentity
51 
52 object NotificationList {
53     object Elements {
54         val Notifications = ElementKey.withIdentity { it is NotificationIdentity }
55     }
56 }
57 
58 @Composable
ContentScopenull59 fun ContentScope.NotificationList(
60     notifications: List<NotificationViewModel>,
61     maxNotificationCount: Int?,
62     demoConfiguration: DemoConfiguration,
63     modifier: Modifier = Modifier,
64     isScrollable: Boolean = true,
65     overscrollEffect: OverscrollEffect? = null,
66 ) {
67     if (demoConfiguration.interactiveNotifications) {
68         ExpandFirstNotificationWhenSwipingFromLockscreenToShade(notifications)
69     }
70 
71     // TODO(b/291025415): Do not share elements that are laid out fully outside the
72     // SceneTransitionLayout bounds.
73     // TODO(b/291025415): Make sure everything still works when using `LazyColumn` instead of a
74     // scrollable `Column`.
75     val scrollState = if (isScrollable) rememberScrollState() else null
76     Column(
77         modifier
78             .thenIf(scrollState != null) {
79                 Modifier.disableSwipesWhenScrolling(NestedScrollableBound.BottomRight)
80                     .verticalScroll(scrollState!!, overscrollEffect?.withoutVisualEffect())
81             }
82             .fillMaxWidth()
83             .padding(16.dp),
84         verticalArrangement = Arrangement.spacedBy(2.dp),
85     ) {
86         val n = maxNotificationCount ?: notifications.size
87         repeat(n) { i ->
88             val notification = notifications[i]
89             val isFirst = i == 0
90             val isLast = i == n - 1
91 
92             key(notification.key) {
93                 Notification(
94                     notification,
95                     isFirst,
96                     isLast,
97                     // Make sure that notifications that are shared (which are always the first
98                     // n notifications) are always drawn above the notifications that are not
99                     // shared.
100                     Modifier.zIndex((n - i).toFloat()),
101                 )
102             }
103         }
104     }
105 }
106 
107 @Composable
ContentScopenull108 private fun ContentScope.ExpandFirstNotificationWhenSwipingFromLockscreenToShade(
109     notifications: List<NotificationViewModel>
110 ) {
111     val firstNotification = notifications.firstOrNull() ?: return
112     val coroutineScope = rememberCoroutineScope()
113     val layoutState = layoutState
114 
115     LaunchedEffect(coroutineScope, layoutState, firstNotification, firstNotification.isExpanded) {
116         if (!firstNotification.isExpanded) {
117             expandFirstNotificationWhenSwipingFromLockscreenToShade(
118                 coroutineScope,
119                 layoutState,
120                 firstNotification,
121             )
122         }
123     }
124 }
125 
expandFirstNotificationWhenSwipingFromLockscreenToShadenull126 private suspend fun expandFirstNotificationWhenSwipingFromLockscreenToShade(
127     coroutineScope: CoroutineScope,
128     layoutState: SceneTransitionLayoutState,
129     firstNotification: NotificationViewModel,
130 ) {
131     // Wait for the user to release their finger during the next swipe transition from Lockscreen to
132     // Shade.
133     layoutState
134         .observableTransitionState()
135         .filterIsInstance<ObservableTransitionState.Transition.ChangeScene>()
136         .filter {
137             it.isInitiatedByUserInput &&
138                 it.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade)
139         }
140         .flatMapLatest { it.isUserInputOngoing.combine(it.currentScene) { a, b -> a to b } }
141         .first { (isUserInputOngoing, currentScene) ->
142             !isUserInputOngoing && currentScene == Scenes.Shade
143         }
144 
145     // Expand the first notification.
146     firstNotification.state.setTargetScene(Notification.Scenes.Expanded, coroutineScope)
147 }
148