1 /*
2 * Copyright (C) 2025 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.systemui.notifications.ui.composable.row
18
19 import android.graphics.drawable.Drawable
20 import androidx.compose.foundation.Image
21 import androidx.compose.foundation.background
22 import androidx.compose.foundation.border
23 import androidx.compose.foundation.isSystemInDarkTheme
24 import androidx.compose.foundation.layout.Box
25 import androidx.compose.foundation.layout.Row
26 import androidx.compose.foundation.layout.fillMaxSize
27 import androidx.compose.foundation.layout.padding
28 import androidx.compose.foundation.layout.size
29 import androidx.compose.foundation.shape.CircleShape
30 import androidx.compose.material.icons.Icons
31 import androidx.compose.material.icons.filled.ExpandMore
32 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
33 import androidx.compose.material3.Icon
34 import androidx.compose.material3.MaterialTheme
35 import androidx.compose.material3.Text
36 import androidx.compose.runtime.Composable
37 import androidx.compose.runtime.ReadOnlyComposable
38 import androidx.compose.runtime.getValue
39 import androidx.compose.ui.Alignment
40 import androidx.compose.ui.Modifier
41 import androidx.compose.ui.draw.clip
42 import androidx.compose.ui.draw.drawBehind
43 import androidx.compose.ui.geometry.CornerRadius
44 import androidx.compose.ui.graphics.Color
45 import androidx.compose.ui.graphics.ColorFilter
46 import androidx.compose.ui.graphics.graphicsLayer
47 import androidx.compose.ui.layout.ContentScale
48 import androidx.compose.ui.platform.LocalDensity
49 import androidx.compose.ui.unit.dp
50 import androidx.compose.ui.unit.sp
51 import com.android.compose.animation.scene.ContentScope
52 import com.android.compose.animation.scene.ElementKey
53 import com.android.compose.animation.scene.LowestZIndexContentPicker
54 import com.android.compose.animation.scene.ValueKey
55 import com.android.compose.animation.scene.animateElementColorAsState
56 import com.android.compose.animation.scene.animateElementFloatAsState
57 import com.android.compose.ui.graphics.painter.rememberDrawablePainter
58
59 object NotificationRowPrimitives {
60 object Elements {
61 val PillBackground = ElementKey("PillBackground", contentPicker = LowestZIndexContentPicker)
62 val NotificationIconBackground = ElementKey("NotificationIconBackground")
63 val Chevron = ElementKey("Chevron")
64 }
65
66 object Values {
67 val ChevronRotation = ValueKey("NotificationChevronRotation")
68 val PillBackgroundColor = ValueKey("PillBackgroundColor")
69 }
70 }
71
72 /** The Icon displayed at the start of any notification row. */
73 @Composable
ContentScopenull74 fun ContentScope.BundleIcon(drawable: Drawable?, modifier: Modifier = Modifier) {
75 val surfaceColor = notificationElementSurfaceColor()
76 Box(
77 modifier =
78 modifier
79 // Has to be a shared element because we may have semi-transparent background color
80 .element(NotificationRowPrimitives.Elements.NotificationIconBackground)
81 .size(40.dp)
82 .background(color = surfaceColor, shape = CircleShape)
83 ) {
84 if (drawable == null) return@Box
85 val painter = rememberDrawablePainter(drawable)
86 Image(
87 painter = painter,
88 contentDescription = null,
89 modifier = Modifier.padding(10.dp).fillMaxSize(),
90 contentScale = ContentScale.Fit,
91 colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
92 )
93 }
94 }
95
96 /** The Icon used to display a preview of contained child notifications in a Bundle. */
97 @Composable
PreviewIconnull98 fun PreviewIcon(drawable: Drawable, modifier: Modifier = Modifier) {
99 val surfaceColor = notificationElementSurfaceColor()
100 Box(
101 modifier =
102 modifier
103 .background(color = surfaceColor, shape = CircleShape)
104 .border(0.5.dp, surfaceColor, CircleShape)
105 ) {
106 val painter = rememberDrawablePainter(drawable)
107 Image(
108 painter = painter,
109 contentDescription = null,
110 modifier = Modifier.fillMaxSize().clip(CircleShape),
111 contentScale = ContentScale.Fit,
112 )
113 }
114 }
115
116 /** The ExpansionControl of any expandable notification row, containing a Chevron. */
117 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
118 @Composable
ContentScopenull119 fun ContentScope.ExpansionControl(
120 collapsed: Boolean,
121 hasUnread: Boolean,
122 numberToShow: Int?,
123 modifier: Modifier = Modifier,
124 ) {
125 val textColor =
126 if (hasUnread) MaterialTheme.colorScheme.onTertiary else MaterialTheme.colorScheme.onSurface
127 Box(modifier = modifier) {
128 // The background is a shared Element and therefore can't be the parent of a different
129 // shared Element (the chevron), otherwise the child can't be animated.
130 PillBackground(hasUnread, modifier = Modifier.matchParentSize())
131 Row(
132 verticalAlignment = Alignment.CenterVertically,
133 modifier = Modifier.padding(vertical = 2.dp, horizontal = 6.dp),
134 ) {
135 val iconSizeDp = with(LocalDensity.current) { 16.sp.toDp() }
136
137 if (numberToShow != null) {
138 Text(
139 text = numberToShow.toString(),
140 style = MaterialTheme.typography.labelSmallEmphasized,
141 color = textColor,
142 modifier = Modifier.padding(end = 2.dp),
143 )
144 }
145 Chevron(collapsed = collapsed, modifier = Modifier.size(iconSizeDp), color = textColor)
146 }
147 }
148 }
149
150 @Composable
PillBackgroundnull151 private fun ContentScope.PillBackground(hasUnread: Boolean, modifier: Modifier = Modifier) {
152 ElementWithValues(NotificationRowPrimitives.Elements.PillBackground, modifier) {
153 val bgColorNoUnread = notificationElementSurfaceColor()
154 val surfaceColor by
155 animateElementColorAsState(
156 if (hasUnread) MaterialTheme.colorScheme.tertiary else bgColorNoUnread,
157 NotificationRowPrimitives.Values.PillBackgroundColor,
158 )
159 content {
160 Box(
161 modifier =
162 Modifier.drawBehind {
163 drawRoundRect(
164 color = surfaceColor,
165 cornerRadius = CornerRadius(100.dp.toPx(), 100.dp.toPx()),
166 )
167 }
168 )
169 }
170 }
171 }
172
173 @Composable
174 @ReadOnlyComposable
notificationElementSurfaceColornull175 private fun notificationElementSurfaceColor(): Color {
176 return if (isSystemInDarkTheme()) {
177 Color.White.copy(alpha = 0.15f)
178 } else {
179 MaterialTheme.colorScheme.surfaceContainerHighest
180 }
181 }
182
183 @Composable
ContentScopenull184 private fun ContentScope.Chevron(collapsed: Boolean, color: Color, modifier: Modifier = Modifier) {
185 val key = NotificationRowPrimitives.Elements.Chevron
186 ElementWithValues(key, modifier) {
187 val rotation by
188 animateElementFloatAsState(
189 if (collapsed) 0f else 180f,
190 NotificationRowPrimitives.Values.ChevronRotation,
191 )
192 content {
193 Icon(
194 imageVector = Icons.Default.ExpandMore,
195 contentDescription = null,
196 modifier = Modifier.graphicsLayer { rotationZ = rotation },
197 tint = color,
198 )
199 }
200 }
201 }
202