• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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.content.Context
20 import android.graphics.drawable.Drawable
21 import androidx.compose.foundation.Image
22 import androidx.compose.foundation.clickable
23 import androidx.compose.foundation.layout.Box
24 import androidx.compose.foundation.layout.Row
25 import androidx.compose.foundation.layout.padding
26 import androidx.compose.foundation.layout.size
27 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
28 import androidx.compose.material3.MaterialTheme
29 import androidx.compose.material3.Text
30 import androidx.compose.runtime.Composable
31 import androidx.compose.runtime.rememberCoroutineScope
32 import androidx.compose.ui.Alignment
33 import androidx.compose.ui.Modifier
34 import androidx.compose.ui.layout.ContentScale
35 import androidx.compose.ui.layout.Layout
36 import androidx.compose.ui.platform.ComposeView
37 import androidx.compose.ui.text.style.TextOverflow
38 import androidx.compose.ui.unit.constrainHeight
39 import androidx.compose.ui.unit.constrainWidth
40 import androidx.compose.ui.unit.dp
41 import androidx.compose.ui.util.fastForEach
42 import androidx.compose.ui.util.fastMap
43 import androidx.compose.ui.util.fastMaxOfOrDefault
44 import androidx.compose.ui.util.fastSumBy
45 import com.android.compose.animation.scene.ContentScope
46 import com.android.compose.animation.scene.ElementKey
47 import com.android.compose.animation.scene.SceneKey
48 import com.android.compose.animation.scene.SceneTransitionLayout
49 import com.android.compose.theme.PlatformTheme
50 import com.android.compose.ui.graphics.painter.rememberDrawablePainter
51 import com.android.systemui.statusbar.notification.row.ui.viewmodel.BundleHeaderViewModel
52 
53 object BundleHeader {
54     object Scenes {
55         val Collapsed = SceneKey("Collapsed")
56         val Expanded = SceneKey("Expanded")
57     }
58 
59     object Elements {
60         val PreviewIcon1 = ElementKey("PreviewIcon1")
61         val PreviewIcon2 = ElementKey("PreviewIcon2")
62         val PreviewIcon3 = ElementKey("PreviewIcon3")
63         val TitleText = ElementKey("TitleText")
64     }
65 }
66 
createComposeViewnull67 fun createComposeView(viewModel: BundleHeaderViewModel, context: Context): ComposeView {
68     // TODO(b/399588047): Check if we can init PlatformTheme once instead of once per ComposeView
69     return ComposeView(context).apply { setContent { PlatformTheme { BundleHeader(viewModel) } } }
70 }
71 
72 @Composable
BundleHeadernull73 fun BundleHeader(viewModel: BundleHeaderViewModel, modifier: Modifier = Modifier) {
74     Box(modifier) {
75         Background(background = viewModel.backgroundDrawable, modifier = Modifier.matchParentSize())
76         val scope = rememberCoroutineScope()
77         SceneTransitionLayout(
78             state = viewModel.state,
79             modifier =
80                 Modifier.clickable(
81                     onClick = { viewModel.onHeaderClicked(scope) },
82                     interactionSource = null,
83                     indication = null,
84                 ),
85         ) {
86             scene(BundleHeader.Scenes.Collapsed) {
87                 BundleHeaderContent(viewModel, collapsed = true)
88             }
89             scene(BundleHeader.Scenes.Expanded) {
90                 BundleHeaderContent(viewModel, collapsed = false)
91             }
92         }
93     }
94 }
95 
96 @Composable
Backgroundnull97 private fun Background(background: Drawable?, modifier: Modifier = Modifier) {
98     if (background != null) {
99         val painter = rememberDrawablePainter(drawable = background)
100         Image(
101             painter = painter,
102             contentDescription = null,
103             contentScale = ContentScale.Crop,
104             modifier = modifier,
105         )
106     }
107 }
108 
109 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
110 @Composable
ContentScopenull111 private fun ContentScope.BundleHeaderContent(
112     viewModel: BundleHeaderViewModel,
113     collapsed: Boolean,
114     modifier: Modifier = Modifier,
115 ) {
116     Row(
117         verticalAlignment = Alignment.CenterVertically,
118         modifier = modifier.padding(vertical = 16.dp),
119     ) {
120         BundleIcon(viewModel.bundleIcon, modifier = Modifier.padding(horizontal = 16.dp))
121         Text(
122             text = viewModel.titleText,
123             style = MaterialTheme.typography.titleMediumEmphasized,
124             color = MaterialTheme.colorScheme.primary,
125             overflow = TextOverflow.Ellipsis,
126             maxLines = 1,
127             modifier = Modifier.element(BundleHeader.Elements.TitleText).weight(1f),
128         )
129 
130         if (collapsed && viewModel.previewIcons.isNotEmpty()) {
131             BundlePreviewIcons(
132                 previewDrawables = viewModel.previewIcons,
133                 modifier = Modifier.padding(start = 8.dp),
134             )
135         }
136 
137         ExpansionControl(
138             collapsed = collapsed,
139             hasUnread = viewModel.hasUnreadMessages,
140             numberToShow = viewModel.numberOfChildren,
141             modifier = Modifier.padding(start = 8.dp, end = 16.dp),
142         )
143     }
144 }
145 
146 @Composable
ContentScopenull147 private fun ContentScope.BundlePreviewIcons(
148     previewDrawables: List<Drawable>,
149     modifier: Modifier = Modifier,
150 ) {
151     check(previewDrawables.isNotEmpty())
152     val iconSize = 32.dp
153     HalfOverlappingReversedRow(modifier = modifier) {
154         PreviewIcon(
155             drawable = previewDrawables[0],
156             modifier = Modifier.element(BundleHeader.Elements.PreviewIcon1).size(iconSize),
157         )
158         if (previewDrawables.size < 2) return@HalfOverlappingReversedRow
159         PreviewIcon(
160             drawable = previewDrawables[1],
161             modifier = Modifier.element(BundleHeader.Elements.PreviewIcon2).size(iconSize),
162         )
163         if (previewDrawables.size < 3) return@HalfOverlappingReversedRow
164         PreviewIcon(
165             drawable = previewDrawables[2],
166             modifier = Modifier.element(BundleHeader.Elements.PreviewIcon3).size(iconSize),
167         )
168     }
169 }
170 
171 @Composable
HalfOverlappingReversedRownull172 private fun HalfOverlappingReversedRow(
173     modifier: Modifier = Modifier,
174     content: @Composable () -> Unit,
175 ) {
176     Layout(modifier = modifier, content = content) { measurables, constraints ->
177         val placeables = measurables.fastMap { measurable -> measurable.measure(constraints) }
178 
179         if (placeables.isEmpty())
180             return@Layout layout(constraints.minWidth, constraints.minHeight) {}
181         val width = placeables.fastSumBy { it.width / 2 } + placeables.first().width / 2
182         val childHeight = placeables.fastMaxOfOrDefault(0) { it.height }
183 
184         layout(constraints.constrainWidth(width), constraints.constrainHeight(childHeight)) {
185             // Start in the middle of the right-most placeable
186             var currentXPosition = placeables.fastSumBy { it.width / 2 }
187             placeables.fastForEach { placeable ->
188                 currentXPosition -= placeable.width / 2
189                 placeable.placeRelative(x = currentXPosition, y = 0)
190             }
191         }
192     }
193 }
194