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