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