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