1 /*
<lambda>null2  * Copyright 2021 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 androidx.compose.material
18 
19 import androidx.compose.foundation.background
20 import androidx.compose.foundation.layout.Arrangement
21 import androidx.compose.foundation.layout.Box
22 import androidx.compose.foundation.layout.BoxScope
23 import androidx.compose.foundation.layout.Row
24 import androidx.compose.foundation.layout.RowScope
25 import androidx.compose.foundation.layout.defaultMinSize
26 import androidx.compose.foundation.layout.padding
27 import androidx.compose.foundation.shape.RoundedCornerShape
28 import androidx.compose.runtime.Composable
29 import androidx.compose.runtime.CompositionLocalProvider
30 import androidx.compose.ui.Alignment
31 import androidx.compose.ui.Modifier
32 import androidx.compose.ui.draw.clip
33 import androidx.compose.ui.graphics.Color
34 import androidx.compose.ui.layout.FirstBaseline
35 import androidx.compose.ui.layout.LastBaseline
36 import androidx.compose.ui.layout.Layout
37 import androidx.compose.ui.layout.layoutId
38 import androidx.compose.ui.unit.dp
39 import androidx.compose.ui.unit.sp
40 import androidx.compose.ui.util.fastFirst
41 
42 /**
43  * A BadgeBox is used to decorate [content] with a [badge] that can contain dynamic information,
44  * such as the presence of a new notification or a number of pending requests. Badges can be icon
45  * only or contain short text.
46  *
47  * A common use case is to display a badge with bottom navigation items. For more information, see
48  * [Bottom Navigation](https://material.io/components/bottom-navigation#behavior)
49  *
50  * A simple icon with badge example looks like:
51  *
52  * @sample androidx.compose.material.samples.BottomNavigationItemWithBadge
53  * @param badge the badge to be displayed - typically a [Badge]
54  * @param modifier optional [Modifier] for this item
55  * @param content the anchor to which this badge will be positioned
56  */
57 @Composable
58 fun BadgedBox(
59     badge: @Composable BoxScope.() -> Unit,
60     modifier: Modifier = Modifier,
61     content: @Composable BoxScope.() -> Unit,
62 ) {
63     Layout(
64         {
65             Box(
66                 modifier = Modifier.layoutId("anchor"),
67                 contentAlignment = Alignment.Center,
68                 content = content
69             )
70             Box(modifier = Modifier.layoutId("badge"), content = badge)
71         },
72         modifier = modifier
73     ) { measurables, constraints ->
74         val badgePlaceable =
75             measurables
76                 .fastFirst { it.layoutId == "badge" }
77                 .measure(
78                     // Measure with loose constraints for height as we don't want the text to take
79                     // up more
80                     // space than it needs.
81                     constraints.copy(minHeight = 0)
82                 )
83 
84         val anchorPlaceable = measurables.fastFirst { it.layoutId == "anchor" }.measure(constraints)
85 
86         val firstBaseline = anchorPlaceable[FirstBaseline]
87         val lastBaseline = anchorPlaceable[LastBaseline]
88         val totalWidth = anchorPlaceable.width
89         val totalHeight = anchorPlaceable.height
90 
91         layout(
92             totalWidth,
93             totalHeight,
94             // Provide custom baselines based only on the anchor content to avoid default baseline
95             // calculations from including by any badge content.
96             mapOf(FirstBaseline to firstBaseline, LastBaseline to lastBaseline)
97         ) {
98             // Use the width of the badge to infer whether it has any content (based on radius used
99             // in [Badge]) and determine its horizontal offset.
100             val hasContent = badgePlaceable.width > (2 * BadgeRadius.roundToPx())
101             val badgeHorizontalOffset =
102                 if (hasContent) BadgeWithContentHorizontalOffset else BadgeHorizontalOffset
103 
104             anchorPlaceable.placeRelative(0, 0)
105             val badgeX = anchorPlaceable.width + badgeHorizontalOffset.roundToPx()
106             val badgeY = -badgePlaceable.height / 2
107             badgePlaceable.placeRelative(badgeX, badgeY)
108         }
109     }
110 }
111 
112 /**
113  * Badge is a component that can contain dynamic information, such as the presence of a new
114  * notification or a number of pending requests. Badges can be icon only or contain short text.
115  *
116  * See [BadgedBox] for a top level layout that will properly place the badge relative to content
117  * such as text or an icon.
118  *
119  * @param modifier optional [Modifier] for this item
120  * @param backgroundColor the background color for the badge
121  * @param contentColor the color of label text rendered in the badge
122  * @param content optional content to be rendered inside the badge
123  */
124 @Composable
Badgenull125 fun Badge(
126     modifier: Modifier = Modifier,
127     backgroundColor: Color = MaterialTheme.colors.error,
128     contentColor: Color = contentColorFor(backgroundColor),
129     content: @Composable (RowScope.() -> Unit)? = null,
130 ) {
131     val radius = if (content != null) BadgeWithContentRadius else BadgeRadius
132     val shape = RoundedCornerShape(radius)
133 
134     // Draw badge container.
135     Row(
136         modifier =
137             modifier
138                 .defaultMinSize(minWidth = radius * 2, minHeight = radius * 2)
139                 .background(color = backgroundColor, shape = shape)
140                 .clip(shape)
141                 .padding(horizontal = BadgeWithContentHorizontalPadding),
142         verticalAlignment = Alignment.CenterVertically,
143         horizontalArrangement = Arrangement.Center
144     ) {
145         if (content != null) {
146             CompositionLocalProvider(LocalContentColor provides contentColor) {
147                 val style = MaterialTheme.typography.button.copy(fontSize = BadgeContentFontSize)
148                 ProvideTextStyle(value = style, content = { content() })
149             }
150         }
151     }
152 }
153 
154 /*@VisibleForTesting*/
155 internal val BadgeRadius = 4.dp
156 
157 /*@VisibleForTesting*/
158 internal val BadgeWithContentRadius = 8.dp
159 private val BadgeContentFontSize = 10.sp
160 
161 /*@VisibleForTesting*/
162 // Leading and trailing text padding when a badge is displaying text that is too long to fit in
163 // a circular badge, e.g. if badge number is greater than 9.
164 internal val BadgeWithContentHorizontalPadding = 4.dp
165 
166 /*@VisibleForTesting*/
167 // Horizontally align start/end of text badge 6dp from the end/start edge of its anchor
168 internal val BadgeWithContentHorizontalOffset = -6.dp
169 
170 /*@VisibleForTesting*/
171 // Horizontally align start/end of icon only badge 4dp from the end/start edge of anchor
172 internal val BadgeHorizontalOffset = -4.dp
173