Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 1 | /* |
| 2 | * 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.material3 |
| 18 | |
| 19 | import androidx.compose.animation.animateColorAsState |
| 20 | import androidx.compose.animation.core.animateFloatAsState |
| 21 | import androidx.compose.animation.core.tween |
| 22 | import androidx.compose.foundation.background |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 23 | import androidx.compose.foundation.indication |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 24 | import androidx.compose.foundation.interaction.Interaction |
| 25 | import androidx.compose.foundation.interaction.MutableInteractionSource |
| 26 | import androidx.compose.foundation.layout.Arrangement |
| 27 | import androidx.compose.foundation.layout.Box |
| 28 | import androidx.compose.foundation.layout.Row |
| 29 | import androidx.compose.foundation.layout.RowScope |
Matvei Malkov | 848ce53 | 2022-08-04 16:56:35 +0100 | [diff] [blame] | 30 | import androidx.compose.foundation.layout.WindowInsets |
| 31 | import androidx.compose.foundation.layout.WindowInsetsSides |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 32 | import androidx.compose.foundation.layout.fillMaxWidth |
| 33 | import androidx.compose.foundation.layout.height |
Matvei Malkov | 848ce53 | 2022-08-04 16:56:35 +0100 | [diff] [blame] | 34 | import androidx.compose.foundation.layout.only |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 35 | import androidx.compose.foundation.layout.padding |
Matvei Malkov | 848ce53 | 2022-08-04 16:56:35 +0100 | [diff] [blame] | 36 | import androidx.compose.foundation.layout.windowInsetsPadding |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 37 | import androidx.compose.foundation.selection.selectable |
| 38 | import androidx.compose.foundation.selection.selectableGroup |
| 39 | import androidx.compose.material.ripple.rememberRipple |
Jose Alba Aguado | fe2421c | 2021-11-02 17:37:45 +0100 | [diff] [blame] | 40 | import androidx.compose.material3.tokens.NavigationBarTokens |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 41 | import androidx.compose.runtime.Composable |
| 42 | import androidx.compose.runtime.CompositionLocalProvider |
| 43 | import androidx.compose.runtime.Stable |
| 44 | import androidx.compose.runtime.State |
| 45 | import androidx.compose.runtime.getValue |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 46 | import androidx.compose.runtime.mutableStateOf |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 47 | import androidx.compose.runtime.remember |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 48 | import androidx.compose.runtime.setValue |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 49 | import androidx.compose.ui.Alignment |
| 50 | import androidx.compose.ui.Modifier |
| 51 | import androidx.compose.ui.draw.alpha |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 52 | import androidx.compose.ui.draw.clip |
| 53 | import androidx.compose.ui.geometry.Offset |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 54 | import androidx.compose.ui.graphics.Color |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 55 | import androidx.compose.ui.layout.Layout |
| 56 | import androidx.compose.ui.layout.MeasureResult |
| 57 | import androidx.compose.ui.layout.MeasureScope |
| 58 | import androidx.compose.ui.layout.Placeable |
| 59 | import androidx.compose.ui.layout.layoutId |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 60 | import androidx.compose.ui.layout.onSizeChanged |
| 61 | import androidx.compose.ui.platform.LocalDensity |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 62 | import androidx.compose.ui.semantics.Role |
Max Alfonso-Ying | 35550f6 | 2022-07-14 20:37:06 +0000 | [diff] [blame] | 63 | import androidx.compose.ui.semantics.clearAndSetSemantics |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 64 | import androidx.compose.ui.unit.Constraints |
| 65 | import androidx.compose.ui.unit.Dp |
| 66 | import androidx.compose.ui.unit.dp |
| 67 | import kotlin.math.roundToInt |
| 68 | |
| 69 | /** |
Shalom Gibly | f42290e | 2022-04-12 02:12:48 -0700 | [diff] [blame] | 70 | * <a href="https://m3.material.io/components/navigation-bar/overview" class="external" target="_blank">Material Design bottom navigation bar</a>. |
| 71 | * |
| 72 | * Navigation bars offer a persistent and convenient way to switch between primary destinations in |
| 73 | * an app. |
| 74 | * |
Nick Rout | 2f69d940 | 2021-10-19 16:25:18 +0200 | [diff] [blame] | 75 | * ![Navigation bar image](https://developer.android.com/images/reference/androidx/compose/material3/navigation-bar.png) |
| 76 | * |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 77 | * [NavigationBar] should contain three to five [NavigationBarItem]s, each representing a singular |
| 78 | * destination. |
| 79 | * |
| 80 | * A simple example looks like: |
| 81 | * @sample androidx.compose.material3.samples.NavigationBarSample |
| 82 | * |
| 83 | * See [NavigationBarItem] for configuration specific to each item, and not the overall |
| 84 | * [NavigationBar] component. |
| 85 | * |
Max Ying | 2384bc8 | 2022-04-28 20:20:58 +0000 | [diff] [blame] | 86 | * @param modifier the [Modifier] to be applied to this navigation bar |
| 87 | * @param containerColor the color used for the background of this navigation bar. Use |
| 88 | * [Color.Transparent] to have no color. |
| 89 | * @param contentColor the preferred color for content inside this navigation bar. Defaults to |
| 90 | * either the matching content color for [containerColor], or to the current [LocalContentColor] if |
| 91 | * [containerColor] is not a color from the theme. |
| 92 | * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color |
| 93 | * overlay is applied on top of the container. A higher tonal elevation value will result in a |
| 94 | * darker color in light theme and lighter color in dark theme. See also: [Surface]. |
Matvei Malkov | 848ce53 | 2022-08-04 16:56:35 +0100 | [diff] [blame] | 95 | * @param windowInsets a window insets of the navigation bar. |
Max Ying | 2384bc8 | 2022-04-28 20:20:58 +0000 | [diff] [blame] | 96 | * @param content the content of this navigation bar, typically 3-5 [NavigationBarItem]s |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 97 | */ |
| 98 | @Composable |
| 99 | fun NavigationBar( |
| 100 | modifier: Modifier = Modifier, |
José Figueroa Santos | 49aff52 | 2022-07-19 14:10:38 -0400 | [diff] [blame] | 101 | containerColor: Color = NavigationBarDefaults.containerColor, |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 102 | contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor), |
José Figueroa | 9da8cec | 2022-05-26 14:59:39 -0400 | [diff] [blame] | 103 | tonalElevation: Dp = NavigationBarDefaults.Elevation, |
Matvei Malkov | 848ce53 | 2022-08-04 16:56:35 +0100 | [diff] [blame] | 104 | windowInsets: WindowInsets = NavigationBarDefaults.windowInsets, |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 105 | content: @Composable RowScope.() -> Unit |
| 106 | ) { |
| 107 | Surface( |
| 108 | color = containerColor, |
| 109 | contentColor = contentColor, |
| 110 | tonalElevation = tonalElevation, |
| 111 | modifier = modifier |
| 112 | ) { |
| 113 | Row( |
Matvei Malkov | 848ce53 | 2022-08-04 16:56:35 +0100 | [diff] [blame] | 114 | modifier = Modifier |
| 115 | .fillMaxWidth() |
| 116 | .windowInsetsPadding(windowInsets) |
| 117 | .height(NavigationBarHeight) |
| 118 | .selectableGroup(), |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 119 | horizontalArrangement = Arrangement.spacedBy(NavigationBarItemHorizontalPadding), |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 120 | content = content |
| 121 | ) |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | /** |
| 126 | * Material Design navigation bar item. |
| 127 | * |
Shalom Gibly | f42290e | 2022-04-12 02:12:48 -0700 | [diff] [blame] | 128 | * Navigation bars offer a persistent and convenient way to switch between primary destinations in |
| 129 | * an app. |
| 130 | * |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 131 | * The recommended configuration for a [NavigationBarItem] depends on how many items there are |
| 132 | * inside a [NavigationBar]: |
| 133 | * |
| 134 | * - Three destinations: Display icons and text labels for all destinations. |
| 135 | * - Four destinations: Active destinations display an icon and text label. Inactive destinations |
| 136 | * display icons, and text labels are recommended. |
| 137 | * - Five destinations: Active destinations display an icon and text label. Inactive destinations |
| 138 | * use icons, and use text labels if space permits. |
| 139 | * |
| 140 | * A [NavigationBarItem] always shows text labels (if it exists) when selected. Showing text |
| 141 | * labels if not selected is controlled by [alwaysShowLabel]. |
| 142 | * |
| 143 | * @param selected whether this item is selected |
Max Ying | 2384bc8 | 2022-04-28 20:20:58 +0000 | [diff] [blame] | 144 | * @param onClick called when this item is clicked |
| 145 | * @param icon icon for this item, typically an [Icon] |
| 146 | * @param modifier the [Modifier] to be applied to this item |
| 147 | * @param enabled controls the enabled state of this item. When `false`, this component will not |
| 148 | * respond to user input, and it will appear visually disabled and disabled to accessibility |
| 149 | * services. |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 150 | * @param label optional text label for this item |
Max Ying | 2384bc8 | 2022-04-28 20:20:58 +0000 | [diff] [blame] | 151 | * @param alwaysShowLabel whether to always show the label for this item. If `false`, the label will |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 152 | * only be shown when this item is selected. |
Connie Shi | 9d1b1ad | 2022-07-28 14:33:05 -0400 | [diff] [blame] | 153 | * @param colors [NavigationBarItemColors] that will be used to resolve the colors used for this |
| 154 | * item in different states. See [NavigationBarItemDefaults.colors]. |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 155 | * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s |
Max Ying | 2384bc8 | 2022-04-28 20:20:58 +0000 | [diff] [blame] | 156 | * for this item. You can create and pass in your own `remember`ed instance to observe |
| 157 | * [Interaction]s and customize the appearance / behavior of this item in different states. |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 158 | */ |
| 159 | @Composable |
| 160 | fun RowScope.NavigationBarItem( |
| 161 | selected: Boolean, |
| 162 | onClick: () -> Unit, |
| 163 | icon: @Composable () -> Unit, |
| 164 | modifier: Modifier = Modifier, |
| 165 | enabled: Boolean = true, |
| 166 | label: @Composable (() -> Unit)? = null, |
| 167 | alwaysShowLabel: Boolean = true, |
Connie Shi | 9d1b1ad | 2022-07-28 14:33:05 -0400 | [diff] [blame] | 168 | colors: NavigationBarItemColors = NavigationBarItemDefaults.colors(), |
| 169 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 170 | ) { |
| 171 | val styledIcon = @Composable { |
Max Alfonso-Ying | 781cd8a | 2022-11-14 19:04:31 +0000 | [diff] [blame] | 172 | val iconColor by colors.iconColor(selected = selected, enabled = enabled) |
Max Alfonso-Ying | 35550f6 | 2022-07-14 20:37:06 +0000 | [diff] [blame] | 173 | // If there's a label, don't have a11y services repeat the icon description. |
Max Alfonso-Ying | 9f025b5 | 2022-08-01 18:24:55 +0000 | [diff] [blame] | 174 | val clearSemantics = label != null && (alwaysShowLabel || selected) |
Max Alfonso-Ying | 35550f6 | 2022-07-14 20:37:06 +0000 | [diff] [blame] | 175 | Box(modifier = if (clearSemantics) Modifier.clearAndSetSemantics {} else Modifier) { |
| 176 | CompositionLocalProvider(LocalContentColor provides iconColor, content = icon) |
| 177 | } |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 178 | } |
| 179 | |
| 180 | val styledLabel: @Composable (() -> Unit)? = label?.let { |
| 181 | @Composable { |
Jose Alba Aguado | fe2421c | 2021-11-02 17:37:45 +0100 | [diff] [blame] | 182 | val style = MaterialTheme.typography.fromToken(NavigationBarTokens.LabelTextFont) |
Max Alfonso-Ying | 781cd8a | 2022-11-14 19:04:31 +0000 | [diff] [blame] | 183 | val textColor by colors.textColor(selected = selected, enabled = enabled) |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 184 | CompositionLocalProvider(LocalContentColor provides textColor) { |
| 185 | ProvideTextStyle(style, content = label) |
| 186 | } |
| 187 | } |
| 188 | } |
| 189 | |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 190 | var itemWidth by remember { mutableStateOf(0) } |
| 191 | |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 192 | Box( |
| 193 | modifier |
| 194 | .selectable( |
| 195 | selected = selected, |
| 196 | onClick = onClick, |
| 197 | enabled = enabled, |
| 198 | role = Role.Tab, |
| 199 | interactionSource = interactionSource, |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 200 | indication = null, |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 201 | ) |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 202 | .weight(1f) |
| 203 | .onSizeChanged { |
| 204 | itemWidth = it.width |
| 205 | }, |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 206 | contentAlignment = Alignment.Center |
| 207 | ) { |
| 208 | val animationProgress: Float by animateFloatAsState( |
| 209 | targetValue = if (selected) 1f else 0f, |
| 210 | animationSpec = tween(ItemAnimationDurationMillis) |
| 211 | ) |
| 212 | |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 213 | // The entire item is selectable, but only the indicator pill shows the ripple. To achieve |
| 214 | // this, we re-map the coordinates of the item's InteractionSource into the coordinates of |
| 215 | // the indicator. |
| 216 | val deltaOffset: Offset |
| 217 | with(LocalDensity.current) { |
| 218 | val indicatorWidth = NavigationBarTokens.ActiveIndicatorWidth.roundToPx() |
| 219 | deltaOffset = Offset( |
| 220 | (itemWidth - indicatorWidth).toFloat() / 2, |
| 221 | IndicatorVerticalOffset.toPx() |
| 222 | ) |
| 223 | } |
| 224 | val offsetInteractionSource = remember(interactionSource, deltaOffset) { |
| 225 | MappedInteractionSource(interactionSource, deltaOffset) |
| 226 | } |
| 227 | |
| 228 | // The indicator has a width-expansion animation which interferes with the timing of the |
| 229 | // ripple, which is why they are separate composables |
| 230 | val indicatorRipple = @Composable { |
| 231 | Box( |
Matvei Malkov | 848ce53 | 2022-08-04 16:56:35 +0100 | [diff] [blame] | 232 | Modifier |
| 233 | .layoutId(IndicatorRippleLayoutIdTag) |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 234 | .clip(NavigationBarTokens.ActiveIndicatorShape.toShape()) |
| 235 | .indication(offsetInteractionSource, rememberRipple()) |
| 236 | ) |
| 237 | } |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 238 | val indicator = @Composable { |
| 239 | Box( |
Matvei Malkov | 848ce53 | 2022-08-04 16:56:35 +0100 | [diff] [blame] | 240 | Modifier |
| 241 | .layoutId(IndicatorLayoutIdTag) |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 242 | .background( |
| 243 | color = colors.indicatorColor.copy(alpha = animationProgress), |
Jose Alba Aguado | 3205603 | 2022-03-22 12:50:19 +0100 | [diff] [blame] | 244 | shape = NavigationBarTokens.ActiveIndicatorShape.toShape(), |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 245 | ) |
| 246 | ) |
| 247 | } |
| 248 | |
| 249 | NavigationBarItemBaselineLayout( |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 250 | indicatorRipple = indicatorRipple, |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 251 | indicator = indicator, |
| 252 | icon = styledIcon, |
| 253 | label = styledLabel, |
| 254 | alwaysShowLabel = alwaysShowLabel, |
| 255 | animationProgress = animationProgress |
| 256 | ) |
| 257 | } |
| 258 | } |
| 259 | |
José Figueroa | 9da8cec | 2022-05-26 14:59:39 -0400 | [diff] [blame] | 260 | /** Defaults used in [NavigationBar]. */ |
| 261 | object NavigationBarDefaults { |
José Figueroa | 9da8cec | 2022-05-26 14:59:39 -0400 | [diff] [blame] | 262 | /** Default elevation for a navigation bar. */ |
| 263 | val Elevation: Dp = NavigationBarTokens.ContainerElevation |
José Figueroa Santos | 49aff52 | 2022-07-19 14:10:38 -0400 | [diff] [blame] | 264 | |
| 265 | /** Default color for a navigation bar. */ |
| 266 | val containerColor: Color @Composable get() = NavigationBarTokens.ContainerColor.toColor() |
Matvei Malkov | 848ce53 | 2022-08-04 16:56:35 +0100 | [diff] [blame] | 267 | |
| 268 | /** |
| 269 | * Default window insets to be used and consumed by navigation bar |
| 270 | */ |
| 271 | val windowInsets: WindowInsets |
| 272 | @Composable |
Matvei Malkov | 82cc433 | 2022-08-29 18:15:05 -0400 | [diff] [blame] | 273 | get() = WindowInsets.systemBarsForVisualComponents |
Matvei Malkov | 848ce53 | 2022-08-04 16:56:35 +0100 | [diff] [blame] | 274 | .only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom) |
José Figueroa | 9da8cec | 2022-05-26 14:59:39 -0400 | [diff] [blame] | 275 | } |
| 276 | |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 277 | /** Defaults used in [NavigationBarItem]. */ |
| 278 | object NavigationBarItemDefaults { |
José Figueroa | 9da8cec | 2022-05-26 14:59:39 -0400 | [diff] [blame] | 279 | |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 280 | /** |
| 281 | * Creates a [NavigationBarItemColors] with the provided colors according to the Material |
| 282 | * specification. |
| 283 | * |
| 284 | * @param selectedIconColor the color to use for the icon when the item is selected. |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 285 | * @param selectedTextColor the color to use for the text label when the item is selected. |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 286 | * @param indicatorColor the color to use for the indicator when the item is selected. |
Connie Shi | 9d1b1ad | 2022-07-28 14:33:05 -0400 | [diff] [blame] | 287 | * @param unselectedIconColor the color to use for the icon when the item is unselected. |
| 288 | * @param unselectedTextColor the color to use for the text label when the item is unselected. |
Max Alfonso-Ying | 781cd8a | 2022-11-14 19:04:31 +0000 | [diff] [blame] | 289 | * @param disabledIconColor the color to use for the icon when the item is disabled. |
| 290 | * @param disabledTextColor the color to use for the text label when the item is disabled. |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 291 | * @return the resulting [NavigationBarItemColors] used for [NavigationBarItem] |
| 292 | */ |
| 293 | @Composable |
| 294 | fun colors( |
Mariano | 15a489b | 2022-01-19 13:08:12 -0500 | [diff] [blame] | 295 | selectedIconColor: Color = NavigationBarTokens.ActiveIconColor.toColor(), |
Mariano | 15a489b | 2022-01-19 13:08:12 -0500 | [diff] [blame] | 296 | selectedTextColor: Color = NavigationBarTokens.ActiveLabelTextColor.toColor(), |
Mariano | 15a489b | 2022-01-19 13:08:12 -0500 | [diff] [blame] | 297 | indicatorColor: Color = NavigationBarTokens.ActiveIndicatorColor.toColor(), |
Connie Shi | 9d1b1ad | 2022-07-28 14:33:05 -0400 | [diff] [blame] | 298 | unselectedIconColor: Color = NavigationBarTokens.InactiveIconColor.toColor(), |
| 299 | unselectedTextColor: Color = NavigationBarTokens.InactiveLabelTextColor.toColor(), |
Max Alfonso-Ying | 781cd8a | 2022-11-14 19:04:31 +0000 | [diff] [blame] | 300 | disabledIconColor: Color = unselectedIconColor.copy(alpha = DisabledAlpha), |
| 301 | disabledTextColor: Color = unselectedTextColor.copy(alpha = DisabledAlpha), |
| 302 | ): NavigationBarItemColors = NavigationBarItemColors( |
| 303 | selectedIconColor = selectedIconColor, |
| 304 | selectedTextColor = selectedTextColor, |
| 305 | selectedIndicatorColor = indicatorColor, |
| 306 | unselectedIconColor = unselectedIconColor, |
| 307 | unselectedTextColor = unselectedTextColor, |
| 308 | disabledIconColor = disabledIconColor, |
| 309 | disabledTextColor = disabledTextColor, |
| 310 | ) |
| 311 | |
| 312 | @Deprecated( |
| 313 | "Use overload with disabledIconColor and disabledTextColor", |
| 314 | level = DeprecationLevel.HIDDEN |
| 315 | ) |
| 316 | @Composable |
| 317 | fun colors( |
| 318 | selectedIconColor: Color = NavigationBarTokens.ActiveIconColor.toColor(), |
| 319 | selectedTextColor: Color = NavigationBarTokens.ActiveLabelTextColor.toColor(), |
| 320 | indicatorColor: Color = NavigationBarTokens.ActiveIndicatorColor.toColor(), |
| 321 | unselectedIconColor: Color = NavigationBarTokens.InactiveIconColor.toColor(), |
| 322 | unselectedTextColor: Color = NavigationBarTokens.InactiveLabelTextColor.toColor(), |
Connie Shi | 071c483 | 2022-07-12 19:09:26 -0400 | [diff] [blame] | 323 | ): NavigationBarItemColors = NavigationBarItemColors( |
| 324 | selectedIconColor = selectedIconColor, |
| 325 | selectedTextColor = selectedTextColor, |
| 326 | selectedIndicatorColor = indicatorColor, |
| 327 | unselectedIconColor = unselectedIconColor, |
| 328 | unselectedTextColor = unselectedTextColor, |
Max Alfonso-Ying | 781cd8a | 2022-11-14 19:04:31 +0000 | [diff] [blame] | 329 | disabledIconColor = unselectedIconColor.copy(alpha = DisabledAlpha), |
| 330 | disabledTextColor = unselectedTextColor.copy(alpha = DisabledAlpha), |
Connie Shi | 071c483 | 2022-07-12 19:09:26 -0400 | [diff] [blame] | 331 | ) |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 332 | } |
| 333 | |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 334 | @Stable |
Connie Shi | 071c483 | 2022-07-12 19:09:26 -0400 | [diff] [blame] | 335 | class NavigationBarItemColors internal constructor( |
| 336 | private val selectedIconColor: Color, |
| 337 | private val selectedTextColor: Color, |
| 338 | private val selectedIndicatorColor: Color, |
| 339 | private val unselectedIconColor: Color, |
| 340 | private val unselectedTextColor: Color, |
Max Alfonso-Ying | 781cd8a | 2022-11-14 19:04:31 +0000 | [diff] [blame] | 341 | private val disabledIconColor: Color, |
| 342 | private val disabledTextColor: Color, |
Connie Shi | 071c483 | 2022-07-12 19:09:26 -0400 | [diff] [blame] | 343 | ) { |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 344 | /** |
| 345 | * Represents the icon color for this item, depending on whether it is [selected]. |
| 346 | * |
| 347 | * @param selected whether the item is selected |
Max Alfonso-Ying | 781cd8a | 2022-11-14 19:04:31 +0000 | [diff] [blame] | 348 | * @param enabled whether the item is enabled |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 349 | */ |
| 350 | @Composable |
Max Alfonso-Ying | 781cd8a | 2022-11-14 19:04:31 +0000 | [diff] [blame] | 351 | internal fun iconColor(selected: Boolean, enabled: Boolean): State<Color> { |
| 352 | val targetValue = when { |
| 353 | !enabled -> disabledIconColor |
| 354 | selected -> selectedIconColor |
| 355 | else -> unselectedIconColor |
| 356 | } |
Connie Shi | 071c483 | 2022-07-12 19:09:26 -0400 | [diff] [blame] | 357 | return animateColorAsState( |
Max Alfonso-Ying | 781cd8a | 2022-11-14 19:04:31 +0000 | [diff] [blame] | 358 | targetValue = targetValue, |
Connie Shi | 071c483 | 2022-07-12 19:09:26 -0400 | [diff] [blame] | 359 | animationSpec = tween(ItemAnimationDurationMillis) |
| 360 | ) |
| 361 | } |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 362 | |
| 363 | /** |
| 364 | * Represents the text color for this item, depending on whether it is [selected]. |
| 365 | * |
| 366 | * @param selected whether the item is selected |
Max Alfonso-Ying | 781cd8a | 2022-11-14 19:04:31 +0000 | [diff] [blame] | 367 | * @param enabled whether the item is enabled |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 368 | */ |
| 369 | @Composable |
Max Alfonso-Ying | 781cd8a | 2022-11-14 19:04:31 +0000 | [diff] [blame] | 370 | internal fun textColor(selected: Boolean, enabled: Boolean): State<Color> { |
| 371 | val targetValue = when { |
| 372 | !enabled -> disabledTextColor |
| 373 | selected -> selectedTextColor |
| 374 | else -> unselectedTextColor |
| 375 | } |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 376 | return animateColorAsState( |
Max Alfonso-Ying | 781cd8a | 2022-11-14 19:04:31 +0000 | [diff] [blame] | 377 | targetValue = targetValue, |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 378 | animationSpec = tween(ItemAnimationDurationMillis) |
| 379 | ) |
| 380 | } |
| 381 | |
Connie Shi | 071c483 | 2022-07-12 19:09:26 -0400 | [diff] [blame] | 382 | /** Represents the color of the indicator used for selected items. */ |
| 383 | internal val indicatorColor: Color |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 384 | get() = selectedIndicatorColor |
Connie Shi | 071c483 | 2022-07-12 19:09:26 -0400 | [diff] [blame] | 385 | |
| 386 | override fun equals(other: Any?): Boolean { |
| 387 | if (this === other) return true |
| 388 | if (other == null || other !is NavigationBarItemColors) return false |
| 389 | |
| 390 | if (selectedIconColor != other.selectedIconColor) return false |
| 391 | if (unselectedIconColor != other.unselectedIconColor) return false |
| 392 | if (selectedTextColor != other.selectedTextColor) return false |
| 393 | if (unselectedTextColor != other.unselectedTextColor) return false |
| 394 | if (selectedIndicatorColor != other.selectedIndicatorColor) return false |
Max Alfonso-Ying | 781cd8a | 2022-11-14 19:04:31 +0000 | [diff] [blame] | 395 | if (disabledIconColor != other.disabledIconColor) return false |
| 396 | if (disabledTextColor != other.disabledTextColor) return false |
Connie Shi | 071c483 | 2022-07-12 19:09:26 -0400 | [diff] [blame] | 397 | |
| 398 | return true |
| 399 | } |
| 400 | override fun hashCode(): Int { |
| 401 | var result = selectedIconColor.hashCode() |
| 402 | result = 31 * result + unselectedIconColor.hashCode() |
| 403 | result = 31 * result + selectedTextColor.hashCode() |
| 404 | result = 31 * result + unselectedTextColor.hashCode() |
| 405 | result = 31 * result + selectedIndicatorColor.hashCode() |
Max Alfonso-Ying | 781cd8a | 2022-11-14 19:04:31 +0000 | [diff] [blame] | 406 | result = 31 * result + disabledIconColor.hashCode() |
| 407 | result = 31 * result + disabledTextColor.hashCode() |
Connie Shi | 071c483 | 2022-07-12 19:09:26 -0400 | [diff] [blame] | 408 | |
| 409 | return result |
| 410 | } |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 411 | } |
| 412 | |
| 413 | /** |
| 414 | * Base layout for a [NavigationBarItem]. |
| 415 | * |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 416 | * @param indicatorRipple indicator ripple for this item when it is selected |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 417 | * @param indicator indicator for this item when it is selected |
| 418 | * @param icon icon for this item |
| 419 | * @param label text label for this item |
| 420 | * @param alwaysShowLabel whether to always show the label for this item. If false, the label will |
| 421 | * only be shown when this item is selected. |
| 422 | * @param animationProgress progress of the animation, where 0 represents the unselected state of |
| 423 | * this item and 1 represents the selected state. This value controls other values such as indicator |
| 424 | * size, icon and label positions, etc. |
| 425 | */ |
| 426 | @Composable |
| 427 | private fun NavigationBarItemBaselineLayout( |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 428 | indicatorRipple: @Composable () -> Unit, |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 429 | indicator: @Composable () -> Unit, |
| 430 | icon: @Composable () -> Unit, |
| 431 | label: @Composable (() -> Unit)?, |
| 432 | alwaysShowLabel: Boolean, |
| 433 | animationProgress: Float, |
| 434 | ) { |
| 435 | Layout({ |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 436 | indicatorRipple() |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 437 | if (animationProgress > 0) { |
| 438 | indicator() |
| 439 | } |
| 440 | |
| 441 | Box(Modifier.layoutId(IconLayoutIdTag)) { icon() } |
| 442 | |
| 443 | if (label != null) { |
| 444 | Box( |
Matvei Malkov | 848ce53 | 2022-08-04 16:56:35 +0100 | [diff] [blame] | 445 | Modifier |
| 446 | .layoutId(LabelLayoutIdTag) |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 447 | .alpha(if (alwaysShowLabel) 1f else animationProgress) |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 448 | .padding(horizontal = NavigationBarItemHorizontalPadding / 2) |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 449 | ) { label() } |
| 450 | } |
| 451 | }) { measurables, constraints -> |
| 452 | val iconPlaceable = |
| 453 | measurables.first { it.layoutId == IconLayoutIdTag }.measure(constraints) |
| 454 | |
| 455 | val totalIndicatorWidth = iconPlaceable.width + (IndicatorHorizontalPadding * 2).roundToPx() |
| 456 | val animatedIndicatorWidth = (totalIndicatorWidth * animationProgress).roundToInt() |
| 457 | val indicatorHeight = iconPlaceable.height + (IndicatorVerticalPadding * 2).roundToPx() |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 458 | val indicatorRipplePlaceable = |
| 459 | measurables |
| 460 | .first { it.layoutId == IndicatorRippleLayoutIdTag } |
| 461 | .measure( |
| 462 | Constraints.fixed( |
| 463 | width = totalIndicatorWidth, |
| 464 | height = indicatorHeight |
| 465 | ) |
| 466 | ) |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 467 | val indicatorPlaceable = |
| 468 | measurables |
| 469 | .firstOrNull { it.layoutId == IndicatorLayoutIdTag } |
| 470 | ?.measure( |
| 471 | Constraints.fixed( |
| 472 | width = animatedIndicatorWidth, |
| 473 | height = indicatorHeight |
| 474 | ) |
| 475 | ) |
| 476 | |
| 477 | val labelPlaceable = |
| 478 | label?.let { |
| 479 | measurables |
| 480 | .first { it.layoutId == LabelLayoutIdTag } |
| 481 | .measure( |
Max Ying | 2384bc8 | 2022-04-28 20:20:58 +0000 | [diff] [blame] | 482 | // Measure with loose constraints for height as we don't want the label to |
| 483 | // take up more space than it needs |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 484 | constraints.copy(minHeight = 0) |
| 485 | ) |
| 486 | } |
| 487 | |
| 488 | if (label == null) { |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 489 | placeIcon(iconPlaceable, indicatorRipplePlaceable, indicatorPlaceable, constraints) |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 490 | } else { |
| 491 | placeLabelAndIcon( |
| 492 | labelPlaceable!!, |
| 493 | iconPlaceable, |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 494 | indicatorRipplePlaceable, |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 495 | indicatorPlaceable, |
| 496 | constraints, |
| 497 | alwaysShowLabel, |
| 498 | animationProgress |
| 499 | ) |
| 500 | } |
| 501 | } |
| 502 | } |
| 503 | |
| 504 | /** |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 505 | * Places the provided [Placeable]s in the center of the provided [constraints]. |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 506 | */ |
| 507 | private fun MeasureScope.placeIcon( |
| 508 | iconPlaceable: Placeable, |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 509 | indicatorRipplePlaceable: Placeable, |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 510 | indicatorPlaceable: Placeable?, |
| 511 | constraints: Constraints |
| 512 | ): MeasureResult { |
| 513 | val width = constraints.maxWidth |
| 514 | val height = constraints.maxHeight |
| 515 | |
| 516 | val iconX = (width - iconPlaceable.width) / 2 |
| 517 | val iconY = (height - iconPlaceable.height) / 2 |
| 518 | |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 519 | val rippleX = (width - indicatorRipplePlaceable.width) / 2 |
| 520 | val rippleY = (height - indicatorRipplePlaceable.height) / 2 |
| 521 | |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 522 | return layout(width, height) { |
| 523 | indicatorPlaceable?.let { |
| 524 | val indicatorX = (width - it.width) / 2 |
| 525 | val indicatorY = (height - it.height) / 2 |
| 526 | it.placeRelative(indicatorX, indicatorY) |
| 527 | } |
| 528 | iconPlaceable.placeRelative(iconX, iconY) |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 529 | indicatorRipplePlaceable.placeRelative(rippleX, rippleY) |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 530 | } |
| 531 | } |
| 532 | |
| 533 | /** |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 534 | * Places the provided [Placeable]s in the correct position, depending on [alwaysShowLabel] and |
| 535 | * [animationProgress]. |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 536 | * |
| 537 | * When [alwaysShowLabel] is true, the positions do not move. The [iconPlaceable] will be placed |
| 538 | * near the top of the item and the [labelPlaceable] will be placed near the bottom, according to |
| 539 | * the spec. |
| 540 | * |
| 541 | * When [animationProgress] is 1 (representing the selected state), the positions will be the same |
| 542 | * as above. |
| 543 | * |
| 544 | * Otherwise, when [animationProgress] is 0, [iconPlaceable] will be placed in the center, like in |
| 545 | * [placeIcon], and [labelPlaceable] will not be shown. |
| 546 | * |
| 547 | * When [animationProgress] is animating between these values, [iconPlaceable] and [labelPlaceable] |
| 548 | * will be placed at a corresponding interpolated position. |
| 549 | * |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 550 | * [indicatorRipplePlaceable] and [indicatorPlaceable] will always be placed in such a way that to |
| 551 | * share the same center as [iconPlaceable]. |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 552 | * |
| 553 | * @param labelPlaceable text label placeable inside this item |
| 554 | * @param iconPlaceable icon placeable inside this item |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 555 | * @param indicatorRipplePlaceable indicator ripple placeable inside this item |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 556 | * @param indicatorPlaceable indicator placeable inside this item, if it exists |
| 557 | * @param constraints constraints of the item |
| 558 | * @param alwaysShowLabel whether to always show the label for this item. If true, icon and label |
| 559 | * positions will not change. If false, positions transition between 'centered icon with no label' |
| 560 | * and 'top aligned icon with label'. |
| 561 | * @param animationProgress progress of the animation, where 0 represents the unselected state of |
| 562 | * this item and 1 represents the selected state. Values between 0 and 1 interpolate positions of |
| 563 | * the icon and label. |
| 564 | */ |
| 565 | private fun MeasureScope.placeLabelAndIcon( |
| 566 | labelPlaceable: Placeable, |
| 567 | iconPlaceable: Placeable, |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 568 | indicatorRipplePlaceable: Placeable, |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 569 | indicatorPlaceable: Placeable?, |
| 570 | constraints: Constraints, |
| 571 | alwaysShowLabel: Boolean, |
| 572 | animationProgress: Float, |
| 573 | ): MeasureResult { |
| 574 | val height = constraints.maxHeight |
| 575 | |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 576 | // Label should be `ItemVerticalPadding` from the bottom |
Max Ying | 054270d | 2022-06-28 15:18:44 +0000 | [diff] [blame] | 577 | val labelY = height - labelPlaceable.height - NavigationBarItemVerticalPadding.roundToPx() |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 578 | |
| 579 | // Icon (when selected) should be `ItemVerticalPadding` from the top |
| 580 | val selectedIconY = NavigationBarItemVerticalPadding.roundToPx() |
| 581 | val unselectedIconY = |
| 582 | if (alwaysShowLabel) selectedIconY else (height - iconPlaceable.height) / 2 |
| 583 | |
| 584 | // How far the icon needs to move between unselected and selected states. |
| 585 | val iconDistance = unselectedIconY - selectedIconY |
| 586 | |
| 587 | // The interpolated fraction of iconDistance that all placeables need to move based on |
| 588 | // animationProgress. |
| 589 | val offset = (iconDistance * (1 - animationProgress)).roundToInt() |
| 590 | |
| 591 | val containerWidth = constraints.maxWidth |
| 592 | |
| 593 | val labelX = (containerWidth - labelPlaceable.width) / 2 |
| 594 | val iconX = (containerWidth - iconPlaceable.width) / 2 |
| 595 | |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 596 | val rippleX = (containerWidth - indicatorRipplePlaceable.width) / 2 |
| 597 | val rippleY = selectedIconY - IndicatorVerticalPadding.roundToPx() |
| 598 | |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 599 | return layout(containerWidth, height) { |
| 600 | indicatorPlaceable?.let { |
| 601 | val indicatorX = (containerWidth - it.width) / 2 |
| 602 | val indicatorY = selectedIconY - IndicatorVerticalPadding.roundToPx() |
| 603 | it.placeRelative(indicatorX, indicatorY + offset) |
| 604 | } |
| 605 | if (alwaysShowLabel || animationProgress != 0f) { |
| 606 | labelPlaceable.placeRelative(labelX, labelY + offset) |
| 607 | } |
| 608 | iconPlaceable.placeRelative(iconX, selectedIconY + offset) |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 609 | indicatorRipplePlaceable.placeRelative(rippleX, rippleY + offset) |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 610 | } |
| 611 | } |
| 612 | |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 613 | private const val IndicatorRippleLayoutIdTag: String = "indicatorRipple" |
| 614 | |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 615 | private const val IndicatorLayoutIdTag: String = "indicator" |
| 616 | |
| 617 | private const val IconLayoutIdTag: String = "icon" |
| 618 | |
| 619 | private const val LabelLayoutIdTag: String = "label" |
| 620 | |
Jose Alba Aguado | fe2421c | 2021-11-02 17:37:45 +0100 | [diff] [blame] | 621 | private val NavigationBarHeight: Dp = NavigationBarTokens.ContainerHeight |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 622 | |
| 623 | private const val ItemAnimationDurationMillis: Int = 100 |
| 624 | |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 625 | /*@VisibleForTesting*/ |
| 626 | internal val NavigationBarItemHorizontalPadding: Dp = 8.dp |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 627 | |
| 628 | /*@VisibleForTesting*/ |
| 629 | internal val NavigationBarItemVerticalPadding: Dp = 16.dp |
| 630 | |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 631 | private val IndicatorHorizontalPadding: Dp = |
Jose Alba Aguado | fe2421c | 2021-11-02 17:37:45 +0100 | [diff] [blame] | 632 | (NavigationBarTokens.ActiveIndicatorWidth - NavigationBarTokens.IconSize) / 2 |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 633 | |
| 634 | private val IndicatorVerticalPadding: Dp = |
Max Ying | 51d3d9e | 2022-04-08 17:59:07 +0000 | [diff] [blame] | 635 | (NavigationBarTokens.ActiveIndicatorHeight - NavigationBarTokens.IconSize) / 2 |
| 636 | |
| 637 | private val IndicatorVerticalOffset: Dp = 12.dp |