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 |
| 23 | import androidx.compose.foundation.interaction.Interaction |
| 24 | import androidx.compose.foundation.interaction.MutableInteractionSource |
| 25 | import androidx.compose.foundation.layout.Arrangement |
| 26 | import androidx.compose.foundation.layout.Box |
| 27 | import androidx.compose.foundation.layout.Row |
| 28 | import androidx.compose.foundation.layout.RowScope |
| 29 | import androidx.compose.foundation.layout.fillMaxWidth |
| 30 | import androidx.compose.foundation.layout.height |
| 31 | import androidx.compose.foundation.layout.padding |
| 32 | import androidx.compose.foundation.selection.selectable |
| 33 | import androidx.compose.foundation.selection.selectableGroup |
| 34 | import androidx.compose.material.ripple.rememberRipple |
Jose Alba Aguado | fe2421c | 2021-11-02 17:37:45 +0100 | [diff] [blame^] | 35 | import androidx.compose.material3.tokens.NavigationBarTokens |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 36 | import androidx.compose.runtime.Composable |
| 37 | import androidx.compose.runtime.CompositionLocalProvider |
| 38 | import androidx.compose.runtime.Stable |
| 39 | import androidx.compose.runtime.State |
| 40 | import androidx.compose.runtime.getValue |
| 41 | import androidx.compose.runtime.remember |
| 42 | import androidx.compose.ui.Alignment |
| 43 | import androidx.compose.ui.Modifier |
| 44 | import androidx.compose.ui.draw.alpha |
| 45 | import androidx.compose.ui.graphics.Color |
| 46 | import androidx.compose.ui.graphics.Shape |
| 47 | import androidx.compose.ui.layout.LastBaseline |
| 48 | import androidx.compose.ui.layout.Layout |
| 49 | import androidx.compose.ui.layout.MeasureResult |
| 50 | import androidx.compose.ui.layout.MeasureScope |
| 51 | import androidx.compose.ui.layout.Placeable |
| 52 | import androidx.compose.ui.layout.layoutId |
| 53 | import androidx.compose.ui.semantics.Role |
| 54 | import androidx.compose.ui.unit.Constraints |
| 55 | import androidx.compose.ui.unit.Dp |
| 56 | import androidx.compose.ui.unit.dp |
| 57 | import kotlin.math.roundToInt |
| 58 | |
| 59 | /** |
Nick Rout | 2f69d940 | 2021-10-19 16:25:18 +0200 | [diff] [blame] | 60 | * ![Navigation bar image](https://developer.android.com/images/reference/androidx/compose/material3/navigation-bar.png) |
| 61 | * |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 62 | * Material Design bottom navigation bar. |
| 63 | * |
| 64 | * A bottom navigation bar allows switching between primary destinations in an app. |
| 65 | * |
| 66 | * [NavigationBar] should contain three to five [NavigationBarItem]s, each representing a singular |
| 67 | * destination. |
| 68 | * |
| 69 | * A simple example looks like: |
| 70 | * @sample androidx.compose.material3.samples.NavigationBarSample |
| 71 | * |
| 72 | * See [NavigationBarItem] for configuration specific to each item, and not the overall |
| 73 | * [NavigationBar] component. |
| 74 | * |
| 75 | * @param modifier optional [Modifier] for this NavigationBar |
| 76 | * @param containerColor the container color for this NavigationBar |
| 77 | * @param contentColor the preferred content color provided by this NavigationBar to its children. |
| 78 | * Defaults to either the matching content color for [containerColor], or if [containerColor] is not |
| 79 | * a color from the theme, this will keep the same value set above this NavigationBar. |
| 80 | * @param tonalElevation When [containerColor] is [ColorScheme.surface], a higher tonal elevation |
| 81 | * value will result in a darker color in light theme and lighter color in dark theme. See also: |
| 82 | * [Surface]. |
| 83 | * @param content destinations inside this NavigationBar. This should contain multiple |
| 84 | * [NavigationBarItem]s |
| 85 | */ |
| 86 | @Composable |
| 87 | fun NavigationBar( |
| 88 | modifier: Modifier = Modifier, |
Jose Alba Aguado | fe2421c | 2021-11-02 17:37:45 +0100 | [diff] [blame^] | 89 | containerColor: Color = MaterialTheme.colorScheme.fromToken(NavigationBarTokens.ContainerColor), |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 90 | contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor), |
Jose Alba Aguado | fe2421c | 2021-11-02 17:37:45 +0100 | [diff] [blame^] | 91 | tonalElevation: Dp = NavigationBarTokens.ContainerElevation, |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 92 | content: @Composable RowScope.() -> Unit |
| 93 | ) { |
| 94 | Surface( |
| 95 | color = containerColor, |
| 96 | contentColor = contentColor, |
| 97 | tonalElevation = tonalElevation, |
| 98 | modifier = modifier |
| 99 | ) { |
| 100 | Row( |
| 101 | modifier = Modifier.fillMaxWidth().height(NavigationBarHeight).selectableGroup(), |
| 102 | horizontalArrangement = Arrangement.SpaceBetween, |
| 103 | content = content |
| 104 | ) |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | /** |
| 109 | * Material Design navigation bar item. |
| 110 | * |
| 111 | * The recommended configuration for a [NavigationBarItem] depends on how many items there are |
| 112 | * inside a [NavigationBar]: |
| 113 | * |
| 114 | * - Three destinations: Display icons and text labels for all destinations. |
| 115 | * - Four destinations: Active destinations display an icon and text label. Inactive destinations |
| 116 | * display icons, and text labels are recommended. |
| 117 | * - Five destinations: Active destinations display an icon and text label. Inactive destinations |
| 118 | * use icons, and use text labels if space permits. |
| 119 | * |
| 120 | * A [NavigationBarItem] always shows text labels (if it exists) when selected. Showing text |
| 121 | * labels if not selected is controlled by [alwaysShowLabel]. |
| 122 | * |
| 123 | * @param selected whether this item is selected |
| 124 | * @param onClick the callback to be invoked when this item is selected |
| 125 | * @param icon icon for this item, typically this will be an [Icon] |
| 126 | * @param modifier optional [Modifier] for this item |
| 127 | * @param enabled controls the enabled state of this item. When `false`, this item will not be |
| 128 | * clickable and will appear disabled to accessibility services. |
| 129 | * @param label optional text label for this item |
| 130 | * @param alwaysShowLabel whether to always show the label for this item. If false, the label will |
| 131 | * only be shown when this item is selected. |
| 132 | * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s |
| 133 | * for this NavigationBarItem. You can create and pass in your own remembered |
| 134 | * [MutableInteractionSource] if you want to observe [Interaction]s and customize the appearance / |
| 135 | * behavior of this NavigationBarItem in different [Interaction]s. |
| 136 | * @param colors the various colors used in elements of this item |
| 137 | */ |
| 138 | @Composable |
| 139 | fun RowScope.NavigationBarItem( |
| 140 | selected: Boolean, |
| 141 | onClick: () -> Unit, |
| 142 | icon: @Composable () -> Unit, |
| 143 | modifier: Modifier = Modifier, |
| 144 | enabled: Boolean = true, |
| 145 | label: @Composable (() -> Unit)? = null, |
| 146 | alwaysShowLabel: Boolean = true, |
| 147 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, |
| 148 | colors: NavigationBarItemColors = NavigationBarItemDefaults.colors() |
| 149 | ) { |
| 150 | val styledIcon = @Composable { |
| 151 | val iconColor by colors.iconColor(selected = selected) |
| 152 | CompositionLocalProvider(LocalContentColor provides iconColor, content = icon) |
| 153 | } |
| 154 | |
| 155 | val styledLabel: @Composable (() -> Unit)? = label?.let { |
| 156 | @Composable { |
Jose Alba Aguado | fe2421c | 2021-11-02 17:37:45 +0100 | [diff] [blame^] | 157 | val style = MaterialTheme.typography.fromToken(NavigationBarTokens.LabelTextFont) |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 158 | val textColor by colors.textColor(selected = selected) |
| 159 | CompositionLocalProvider(LocalContentColor provides textColor) { |
| 160 | ProvideTextStyle(style, content = label) |
| 161 | } |
| 162 | } |
| 163 | } |
| 164 | |
| 165 | Box( |
| 166 | modifier |
| 167 | .selectable( |
| 168 | selected = selected, |
| 169 | onClick = onClick, |
| 170 | enabled = enabled, |
| 171 | role = Role.Tab, |
| 172 | interactionSource = interactionSource, |
| 173 | indication = rememberRipple(), |
| 174 | ) |
| 175 | .weight(1f), |
| 176 | contentAlignment = Alignment.Center |
| 177 | ) { |
| 178 | val animationProgress: Float by animateFloatAsState( |
| 179 | targetValue = if (selected) 1f else 0f, |
| 180 | animationSpec = tween(ItemAnimationDurationMillis) |
| 181 | ) |
| 182 | |
| 183 | val indicator = @Composable { |
| 184 | Box( |
| 185 | Modifier.layoutId(IndicatorLayoutIdTag) |
| 186 | .background( |
| 187 | color = colors.indicatorColor.copy(alpha = animationProgress), |
| 188 | shape = IndicatorShape |
| 189 | ) |
| 190 | ) |
| 191 | } |
| 192 | |
| 193 | NavigationBarItemBaselineLayout( |
| 194 | indicator = indicator, |
| 195 | icon = styledIcon, |
| 196 | label = styledLabel, |
| 197 | alwaysShowLabel = alwaysShowLabel, |
| 198 | animationProgress = animationProgress |
| 199 | ) |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | /** Defaults used in [NavigationBarItem]. */ |
| 204 | object NavigationBarItemDefaults { |
| 205 | /** |
| 206 | * Creates a [NavigationBarItemColors] with the provided colors according to the Material |
| 207 | * specification. |
| 208 | * |
| 209 | * @param selectedIconColor the color to use for the icon when the item is selected. |
| 210 | * @param unselectedIconColor the color to use for the icon when the item is unselected. |
| 211 | * @param selectedTextColor the color to use for the text label when the item is selected. |
| 212 | * @param unselectedTextColor the color to use for the text label when the item is unselected. |
| 213 | * @param indicatorColor the color to use for the indicator when the item is selected. |
| 214 | * @return the resulting [NavigationBarItemColors] used for [NavigationBarItem] |
| 215 | */ |
| 216 | @Composable |
| 217 | fun colors( |
| 218 | selectedIconColor: Color = |
Jose Alba Aguado | fe2421c | 2021-11-02 17:37:45 +0100 | [diff] [blame^] | 219 | MaterialTheme.colorScheme.fromToken(NavigationBarTokens.ActiveIconColor), |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 220 | unselectedIconColor: Color = |
Jose Alba Aguado | fe2421c | 2021-11-02 17:37:45 +0100 | [diff] [blame^] | 221 | MaterialTheme.colorScheme.fromToken(NavigationBarTokens.InactiveIconColor), |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 222 | selectedTextColor: Color = |
Jose Alba Aguado | fe2421c | 2021-11-02 17:37:45 +0100 | [diff] [blame^] | 223 | MaterialTheme.colorScheme.fromToken(NavigationBarTokens.ActiveLabelTextColor), |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 224 | unselectedTextColor: Color = |
Jose Alba Aguado | fe2421c | 2021-11-02 17:37:45 +0100 | [diff] [blame^] | 225 | MaterialTheme.colorScheme.fromToken(NavigationBarTokens.InactiveLabelTextColor), |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 226 | indicatorColor: Color = |
Jose Alba Aguado | fe2421c | 2021-11-02 17:37:45 +0100 | [diff] [blame^] | 227 | MaterialTheme.colorScheme.fromToken(NavigationBarTokens.ActiveIndicatorColor), |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 228 | ): NavigationBarItemColors = remember( |
| 229 | selectedIconColor, |
| 230 | unselectedIconColor, |
| 231 | selectedTextColor, |
| 232 | unselectedTextColor, |
| 233 | indicatorColor |
| 234 | ) { |
| 235 | DefaultNavigationBarItemColors( |
| 236 | selectedIconColor = selectedIconColor, |
| 237 | unselectedIconColor = unselectedIconColor, |
| 238 | selectedTextColor = selectedTextColor, |
| 239 | unselectedTextColor = unselectedTextColor, |
| 240 | selectedIndicatorColor = indicatorColor, |
| 241 | ) |
| 242 | } |
| 243 | } |
| 244 | |
| 245 | /** Represents the colors of the various elements of a navigation item. */ |
| 246 | @Stable |
| 247 | interface NavigationBarItemColors { |
| 248 | /** |
| 249 | * Represents the icon color for this item, depending on whether it is [selected]. |
| 250 | * |
| 251 | * @param selected whether the item is selected |
| 252 | */ |
| 253 | @Composable |
| 254 | fun iconColor(selected: Boolean): State<Color> |
| 255 | |
| 256 | /** |
| 257 | * Represents the text color for this item, depending on whether it is [selected]. |
| 258 | * |
| 259 | * @param selected whether the item is selected |
| 260 | */ |
| 261 | @Composable |
| 262 | fun textColor(selected: Boolean): State<Color> |
| 263 | |
| 264 | /** Represents the color of the indicator used for selected items. */ |
| 265 | val indicatorColor: Color |
| 266 | @Composable get |
| 267 | } |
| 268 | |
| 269 | @Stable |
| 270 | private class DefaultNavigationBarItemColors( |
| 271 | private val selectedIconColor: Color, |
| 272 | private val unselectedIconColor: Color, |
| 273 | private val selectedTextColor: Color, |
| 274 | private val unselectedTextColor: Color, |
| 275 | private val selectedIndicatorColor: Color, |
| 276 | ) : NavigationBarItemColors { |
| 277 | @Composable |
| 278 | override fun iconColor(selected: Boolean): State<Color> { |
| 279 | return animateColorAsState( |
| 280 | targetValue = if (selected) selectedIconColor else unselectedIconColor, |
| 281 | animationSpec = tween(ItemAnimationDurationMillis) |
| 282 | ) |
| 283 | } |
| 284 | |
| 285 | @Composable |
| 286 | override fun textColor(selected: Boolean): State<Color> { |
| 287 | return animateColorAsState( |
| 288 | targetValue = if (selected) selectedTextColor else unselectedTextColor, |
| 289 | animationSpec = tween(ItemAnimationDurationMillis) |
| 290 | ) |
| 291 | } |
| 292 | |
| 293 | override val indicatorColor: Color |
| 294 | @Composable |
| 295 | get() = selectedIndicatorColor |
| 296 | } |
| 297 | |
| 298 | /** |
| 299 | * Base layout for a [NavigationBarItem]. |
| 300 | * |
| 301 | * @param indicator indicator for this item when it is selected |
| 302 | * @param icon icon for this item |
| 303 | * @param label text label for this item |
| 304 | * @param alwaysShowLabel whether to always show the label for this item. If false, the label will |
| 305 | * only be shown when this item is selected. |
| 306 | * @param animationProgress progress of the animation, where 0 represents the unselected state of |
| 307 | * this item and 1 represents the selected state. This value controls other values such as indicator |
| 308 | * size, icon and label positions, etc. |
| 309 | */ |
| 310 | @Composable |
| 311 | private fun NavigationBarItemBaselineLayout( |
| 312 | indicator: @Composable () -> Unit, |
| 313 | icon: @Composable () -> Unit, |
| 314 | label: @Composable (() -> Unit)?, |
| 315 | alwaysShowLabel: Boolean, |
| 316 | animationProgress: Float, |
| 317 | ) { |
| 318 | Layout({ |
| 319 | if (animationProgress > 0) { |
| 320 | indicator() |
| 321 | } |
| 322 | |
| 323 | Box(Modifier.layoutId(IconLayoutIdTag)) { icon() } |
| 324 | |
| 325 | if (label != null) { |
| 326 | Box( |
| 327 | Modifier.layoutId(LabelLayoutIdTag) |
| 328 | .alpha(if (alwaysShowLabel) 1f else animationProgress) |
| 329 | .padding(horizontal = NavigationBarItemHorizontalPadding) |
| 330 | ) { label() } |
| 331 | } |
| 332 | }) { measurables, constraints -> |
| 333 | val iconPlaceable = |
| 334 | measurables.first { it.layoutId == IconLayoutIdTag }.measure(constraints) |
| 335 | |
| 336 | val totalIndicatorWidth = iconPlaceable.width + (IndicatorHorizontalPadding * 2).roundToPx() |
| 337 | val animatedIndicatorWidth = (totalIndicatorWidth * animationProgress).roundToInt() |
| 338 | val indicatorHeight = iconPlaceable.height + (IndicatorVerticalPadding * 2).roundToPx() |
| 339 | val indicatorPlaceable = |
| 340 | measurables |
| 341 | .firstOrNull { it.layoutId == IndicatorLayoutIdTag } |
| 342 | ?.measure( |
| 343 | Constraints.fixed( |
| 344 | width = animatedIndicatorWidth, |
| 345 | height = indicatorHeight |
| 346 | ) |
| 347 | ) |
| 348 | |
| 349 | val labelPlaceable = |
| 350 | label?.let { |
| 351 | measurables |
| 352 | .first { it.layoutId == LabelLayoutIdTag } |
| 353 | .measure( |
| 354 | // Measure with loose constraints for height as we don't want the label to take up more |
| 355 | // space than it needs |
| 356 | constraints.copy(minHeight = 0) |
| 357 | ) |
| 358 | } |
| 359 | |
| 360 | if (label == null) { |
| 361 | placeIcon(iconPlaceable, indicatorPlaceable, constraints) |
| 362 | } else { |
| 363 | placeLabelAndIcon( |
| 364 | labelPlaceable!!, |
| 365 | iconPlaceable, |
| 366 | indicatorPlaceable, |
| 367 | constraints, |
| 368 | alwaysShowLabel, |
| 369 | animationProgress |
| 370 | ) |
| 371 | } |
| 372 | } |
| 373 | } |
| 374 | |
| 375 | /** |
| 376 | * Places the provided [iconPlaceable], and possibly [indicatorPlaceable] if it exists, in the |
| 377 | * center of the provided [constraints]. |
| 378 | */ |
| 379 | private fun MeasureScope.placeIcon( |
| 380 | iconPlaceable: Placeable, |
| 381 | indicatorPlaceable: Placeable?, |
| 382 | constraints: Constraints |
| 383 | ): MeasureResult { |
| 384 | val width = constraints.maxWidth |
| 385 | val height = constraints.maxHeight |
| 386 | |
| 387 | val iconX = (width - iconPlaceable.width) / 2 |
| 388 | val iconY = (height - iconPlaceable.height) / 2 |
| 389 | |
| 390 | return layout(width, height) { |
| 391 | indicatorPlaceable?.let { |
| 392 | val indicatorX = (width - it.width) / 2 |
| 393 | val indicatorY = (height - it.height) / 2 |
| 394 | it.placeRelative(indicatorX, indicatorY) |
| 395 | } |
| 396 | iconPlaceable.placeRelative(iconX, iconY) |
| 397 | } |
| 398 | } |
| 399 | |
| 400 | /** |
| 401 | * Places the provided [labelPlaceable], [iconPlaceable], and [indicatorPlaceable] in the correct |
| 402 | * position, depending on [alwaysShowLabel] and [animationProgress]. |
| 403 | * |
| 404 | * When [alwaysShowLabel] is true, the positions do not move. The [iconPlaceable] will be placed |
| 405 | * near the top of the item and the [labelPlaceable] will be placed near the bottom, according to |
| 406 | * the spec. |
| 407 | * |
| 408 | * When [animationProgress] is 1 (representing the selected state), the positions will be the same |
| 409 | * as above. |
| 410 | * |
| 411 | * Otherwise, when [animationProgress] is 0, [iconPlaceable] will be placed in the center, like in |
| 412 | * [placeIcon], and [labelPlaceable] will not be shown. |
| 413 | * |
| 414 | * When [animationProgress] is animating between these values, [iconPlaceable] and [labelPlaceable] |
| 415 | * will be placed at a corresponding interpolated position. |
| 416 | * |
| 417 | * [indicatorPlaceable] will always be placed in such a way that it shares the same center as |
| 418 | * [iconPlaceable]. |
| 419 | * |
| 420 | * @param labelPlaceable text label placeable inside this item |
| 421 | * @param iconPlaceable icon placeable inside this item |
| 422 | * @param indicatorPlaceable indicator placeable inside this item, if it exists |
| 423 | * @param constraints constraints of the item |
| 424 | * @param alwaysShowLabel whether to always show the label for this item. If true, icon and label |
| 425 | * positions will not change. If false, positions transition between 'centered icon with no label' |
| 426 | * and 'top aligned icon with label'. |
| 427 | * @param animationProgress progress of the animation, where 0 represents the unselected state of |
| 428 | * this item and 1 represents the selected state. Values between 0 and 1 interpolate positions of |
| 429 | * the icon and label. |
| 430 | */ |
| 431 | private fun MeasureScope.placeLabelAndIcon( |
| 432 | labelPlaceable: Placeable, |
| 433 | iconPlaceable: Placeable, |
| 434 | indicatorPlaceable: Placeable?, |
| 435 | constraints: Constraints, |
| 436 | alwaysShowLabel: Boolean, |
| 437 | animationProgress: Float, |
| 438 | ): MeasureResult { |
| 439 | val height = constraints.maxHeight |
| 440 | |
| 441 | val baseline = labelPlaceable[LastBaseline] |
| 442 | // Label should be `ItemVerticalPadding` from the bottom |
| 443 | val labelY = height - baseline - NavigationBarItemVerticalPadding.roundToPx() |
| 444 | |
| 445 | // Icon (when selected) should be `ItemVerticalPadding` from the top |
| 446 | val selectedIconY = NavigationBarItemVerticalPadding.roundToPx() |
| 447 | val unselectedIconY = |
| 448 | if (alwaysShowLabel) selectedIconY else (height - iconPlaceable.height) / 2 |
| 449 | |
| 450 | // How far the icon needs to move between unselected and selected states. |
| 451 | val iconDistance = unselectedIconY - selectedIconY |
| 452 | |
| 453 | // The interpolated fraction of iconDistance that all placeables need to move based on |
| 454 | // animationProgress. |
| 455 | val offset = (iconDistance * (1 - animationProgress)).roundToInt() |
| 456 | |
| 457 | val containerWidth = constraints.maxWidth |
| 458 | |
| 459 | val labelX = (containerWidth - labelPlaceable.width) / 2 |
| 460 | val iconX = (containerWidth - iconPlaceable.width) / 2 |
| 461 | |
| 462 | return layout(containerWidth, height) { |
| 463 | indicatorPlaceable?.let { |
| 464 | val indicatorX = (containerWidth - it.width) / 2 |
| 465 | val indicatorY = selectedIconY - IndicatorVerticalPadding.roundToPx() |
| 466 | it.placeRelative(indicatorX, indicatorY + offset) |
| 467 | } |
| 468 | if (alwaysShowLabel || animationProgress != 0f) { |
| 469 | labelPlaceable.placeRelative(labelX, labelY + offset) |
| 470 | } |
| 471 | iconPlaceable.placeRelative(iconX, selectedIconY + offset) |
| 472 | } |
| 473 | } |
| 474 | |
| 475 | private const val IndicatorLayoutIdTag: String = "indicator" |
| 476 | |
| 477 | private const val IconLayoutIdTag: String = "icon" |
| 478 | |
| 479 | private const val LabelLayoutIdTag: String = "label" |
| 480 | |
Jose Alba Aguado | fe2421c | 2021-11-02 17:37:45 +0100 | [diff] [blame^] | 481 | private val NavigationBarHeight: Dp = NavigationBarTokens.ContainerHeight |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 482 | |
| 483 | private const val ItemAnimationDurationMillis: Int = 100 |
| 484 | |
| 485 | private val NavigationBarItemHorizontalPadding: Dp = 4.dp |
| 486 | |
| 487 | /*@VisibleForTesting*/ |
| 488 | internal val NavigationBarItemVerticalPadding: Dp = 16.dp |
| 489 | |
Jose Alba Aguado | fe2421c | 2021-11-02 17:37:45 +0100 | [diff] [blame^] | 490 | private val IndicatorShape: Shape = NavigationBarTokens.ActiveIndicatorShape |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 491 | |
| 492 | private val IndicatorHorizontalPadding: Dp = |
Jose Alba Aguado | fe2421c | 2021-11-02 17:37:45 +0100 | [diff] [blame^] | 493 | (NavigationBarTokens.ActiveIndicatorWidth - NavigationBarTokens.IconSize) / 2 |
Max Ying | cc44d97 | 2021-10-11 13:12:45 -0400 | [diff] [blame] | 494 | |
| 495 | private val IndicatorVerticalPadding: Dp = |
Jose Alba Aguado | fe2421c | 2021-11-02 17:37:45 +0100 | [diff] [blame^] | 496 | (NavigationBarTokens.ActiveIndicatorHeight - NavigationBarTokens.IconSize) / 2 |