• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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