[go: nahoru, domu]

blob: 7209ef9a0464e76da492eb56c827f3ecc0ad5dfe [file] [log] [blame]
Max Yingcc44d972021-10-11 13:12:45 -04001/*
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
17package androidx.compose.material3
18
19import androidx.compose.animation.animateColorAsState
20import androidx.compose.animation.core.animateFloatAsState
21import androidx.compose.animation.core.tween
22import androidx.compose.foundation.background
Max Ying51d3d9e2022-04-08 17:59:07 +000023import androidx.compose.foundation.indication
Max Yingcc44d972021-10-11 13:12:45 -040024import androidx.compose.foundation.interaction.Interaction
25import androidx.compose.foundation.interaction.MutableInteractionSource
26import androidx.compose.foundation.layout.Arrangement
27import androidx.compose.foundation.layout.Box
28import androidx.compose.foundation.layout.Row
29import androidx.compose.foundation.layout.RowScope
30import androidx.compose.foundation.layout.fillMaxWidth
31import androidx.compose.foundation.layout.height
32import androidx.compose.foundation.layout.padding
33import androidx.compose.foundation.selection.selectable
34import androidx.compose.foundation.selection.selectableGroup
35import androidx.compose.material.ripple.rememberRipple
Jose Alba Aguadofe2421c2021-11-02 17:37:45 +010036import androidx.compose.material3.tokens.NavigationBarTokens
Max Yingcc44d972021-10-11 13:12:45 -040037import androidx.compose.runtime.Composable
38import androidx.compose.runtime.CompositionLocalProvider
39import androidx.compose.runtime.Stable
40import androidx.compose.runtime.State
41import androidx.compose.runtime.getValue
Max Ying51d3d9e2022-04-08 17:59:07 +000042import androidx.compose.runtime.mutableStateOf
Max Yingcc44d972021-10-11 13:12:45 -040043import androidx.compose.runtime.remember
Max Ying51d3d9e2022-04-08 17:59:07 +000044import androidx.compose.runtime.setValue
Max Yingcc44d972021-10-11 13:12:45 -040045import androidx.compose.ui.Alignment
46import androidx.compose.ui.Modifier
47import androidx.compose.ui.draw.alpha
Max Ying51d3d9e2022-04-08 17:59:07 +000048import androidx.compose.ui.draw.clip
49import androidx.compose.ui.geometry.Offset
Max Yingcc44d972021-10-11 13:12:45 -040050import androidx.compose.ui.graphics.Color
Max Yingcc44d972021-10-11 13:12:45 -040051import androidx.compose.ui.layout.Layout
52import androidx.compose.ui.layout.MeasureResult
53import androidx.compose.ui.layout.MeasureScope
54import androidx.compose.ui.layout.Placeable
55import androidx.compose.ui.layout.layoutId
Max Ying51d3d9e2022-04-08 17:59:07 +000056import androidx.compose.ui.layout.onSizeChanged
57import androidx.compose.ui.platform.LocalDensity
Max Yingcc44d972021-10-11 13:12:45 -040058import androidx.compose.ui.semantics.Role
Max Alfonso-Ying35550f62022-07-14 20:37:06 +000059import androidx.compose.ui.semantics.clearAndSetSemantics
Max Yingcc44d972021-10-11 13:12:45 -040060import androidx.compose.ui.unit.Constraints
61import androidx.compose.ui.unit.Dp
62import androidx.compose.ui.unit.dp
63import kotlin.math.roundToInt
64
65/**
Shalom Giblyf42290e2022-04-12 02:12:48 -070066 * <a href="https://m3.material.io/components/navigation-bar/overview" class="external" target="_blank">Material Design bottom navigation bar</a>.
67 *
68 * Navigation bars offer a persistent and convenient way to switch between primary destinations in
69 * an app.
70 *
Nick Rout2f69d9402021-10-19 16:25:18 +020071 * ![Navigation bar image](https://developer.android.com/images/reference/androidx/compose/material3/navigation-bar.png)
72 *
Max Yingcc44d972021-10-11 13:12:45 -040073 * [NavigationBar] should contain three to five [NavigationBarItem]s, each representing a singular
74 * destination.
75 *
76 * A simple example looks like:
77 * @sample androidx.compose.material3.samples.NavigationBarSample
78 *
79 * See [NavigationBarItem] for configuration specific to each item, and not the overall
80 * [NavigationBar] component.
81 *
Max Ying2384bc82022-04-28 20:20:58 +000082 * @param modifier the [Modifier] to be applied to this navigation bar
83 * @param containerColor the color used for the background of this navigation bar. Use
84 * [Color.Transparent] to have no color.
85 * @param contentColor the preferred color for content inside this navigation bar. Defaults to
86 * either the matching content color for [containerColor], or to the current [LocalContentColor] if
87 * [containerColor] is not a color from the theme.
88 * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
89 * overlay is applied on top of the container. A higher tonal elevation value will result in a
90 * darker color in light theme and lighter color in dark theme. See also: [Surface].
91 * @param content the content of this navigation bar, typically 3-5 [NavigationBarItem]s
Max Yingcc44d972021-10-11 13:12:45 -040092 */
93@Composable
94fun NavigationBar(
95 modifier: Modifier = Modifier,
José Figueroa Santos49aff522022-07-19 14:10:38 -040096 containerColor: Color = NavigationBarDefaults.containerColor,
Max Yingcc44d972021-10-11 13:12:45 -040097 contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor),
José Figueroa9da8cec2022-05-26 14:59:39 -040098 tonalElevation: Dp = NavigationBarDefaults.Elevation,
Max Yingcc44d972021-10-11 13:12:45 -040099 content: @Composable RowScope.() -> Unit
100) {
101 Surface(
102 color = containerColor,
103 contentColor = contentColor,
104 tonalElevation = tonalElevation,
105 modifier = modifier
106 ) {
107 Row(
108 modifier = Modifier.fillMaxWidth().height(NavigationBarHeight).selectableGroup(),
Max Ying51d3d9e2022-04-08 17:59:07 +0000109 horizontalArrangement = Arrangement.spacedBy(NavigationBarItemHorizontalPadding),
Max Yingcc44d972021-10-11 13:12:45 -0400110 content = content
111 )
112 }
113}
114
115/**
116 * Material Design navigation bar item.
117 *
Shalom Giblyf42290e2022-04-12 02:12:48 -0700118 * Navigation bars offer a persistent and convenient way to switch between primary destinations in
119 * an app.
120 *
Max Yingcc44d972021-10-11 13:12:45 -0400121 * The recommended configuration for a [NavigationBarItem] depends on how many items there are
122 * inside a [NavigationBar]:
123 *
124 * - Three destinations: Display icons and text labels for all destinations.
125 * - Four destinations: Active destinations display an icon and text label. Inactive destinations
126 * display icons, and text labels are recommended.
127 * - Five destinations: Active destinations display an icon and text label. Inactive destinations
128 * use icons, and use text labels if space permits.
129 *
130 * A [NavigationBarItem] always shows text labels (if it exists) when selected. Showing text
131 * labels if not selected is controlled by [alwaysShowLabel].
132 *
133 * @param selected whether this item is selected
Max Ying2384bc82022-04-28 20:20:58 +0000134 * @param onClick called when this item is clicked
135 * @param icon icon for this item, typically an [Icon]
136 * @param modifier the [Modifier] to be applied to this item
137 * @param enabled controls the enabled state of this item. When `false`, this component will not
138 * respond to user input, and it will appear visually disabled and disabled to accessibility
139 * services.
Max Yingcc44d972021-10-11 13:12:45 -0400140 * @param label optional text label for this item
Max Ying2384bc82022-04-28 20:20:58 +0000141 * @param alwaysShowLabel whether to always show the label for this item. If `false`, the label will
Max Yingcc44d972021-10-11 13:12:45 -0400142 * only be shown when this item is selected.
Connie Shi9d1b1ad2022-07-28 14:33:05 -0400143 * @param colors [NavigationBarItemColors] that will be used to resolve the colors used for this
144 * item in different states. See [NavigationBarItemDefaults.colors].
Max Yingcc44d972021-10-11 13:12:45 -0400145 * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
Max Ying2384bc82022-04-28 20:20:58 +0000146 * for this item. You can create and pass in your own `remember`ed instance to observe
147 * [Interaction]s and customize the appearance / behavior of this item in different states.
Max Yingcc44d972021-10-11 13:12:45 -0400148 */
149@Composable
150fun RowScope.NavigationBarItem(
151 selected: Boolean,
152 onClick: () -> Unit,
153 icon: @Composable () -> Unit,
154 modifier: Modifier = Modifier,
155 enabled: Boolean = true,
156 label: @Composable (() -> Unit)? = null,
157 alwaysShowLabel: Boolean = true,
Connie Shi9d1b1ad2022-07-28 14:33:05 -0400158 colors: NavigationBarItemColors = NavigationBarItemDefaults.colors(),
159 interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
Max Yingcc44d972021-10-11 13:12:45 -0400160) {
161 val styledIcon = @Composable {
162 val iconColor by colors.iconColor(selected = selected)
Max Alfonso-Ying35550f62022-07-14 20:37:06 +0000163 // If there's a label, don't have a11y services repeat the icon description.
164 val clearSemantics = alwaysShowLabel || selected
165 Box(modifier = if (clearSemantics) Modifier.clearAndSetSemantics {} else Modifier) {
166 CompositionLocalProvider(LocalContentColor provides iconColor, content = icon)
167 }
Max Yingcc44d972021-10-11 13:12:45 -0400168 }
169
170 val styledLabel: @Composable (() -> Unit)? = label?.let {
171 @Composable {
Jose Alba Aguadofe2421c2021-11-02 17:37:45 +0100172 val style = MaterialTheme.typography.fromToken(NavigationBarTokens.LabelTextFont)
Max Yingcc44d972021-10-11 13:12:45 -0400173 val textColor by colors.textColor(selected = selected)
174 CompositionLocalProvider(LocalContentColor provides textColor) {
175 ProvideTextStyle(style, content = label)
176 }
177 }
178 }
179
Max Ying51d3d9e2022-04-08 17:59:07 +0000180 var itemWidth by remember { mutableStateOf(0) }
181
Max Yingcc44d972021-10-11 13:12:45 -0400182 Box(
183 modifier
184 .selectable(
185 selected = selected,
186 onClick = onClick,
187 enabled = enabled,
188 role = Role.Tab,
189 interactionSource = interactionSource,
Max Ying51d3d9e2022-04-08 17:59:07 +0000190 indication = null,
Max Yingcc44d972021-10-11 13:12:45 -0400191 )
Max Ying51d3d9e2022-04-08 17:59:07 +0000192 .weight(1f)
193 .onSizeChanged {
194 itemWidth = it.width
195 },
Max Yingcc44d972021-10-11 13:12:45 -0400196 contentAlignment = Alignment.Center
197 ) {
198 val animationProgress: Float by animateFloatAsState(
199 targetValue = if (selected) 1f else 0f,
200 animationSpec = tween(ItemAnimationDurationMillis)
201 )
202
Max Ying51d3d9e2022-04-08 17:59:07 +0000203 // The entire item is selectable, but only the indicator pill shows the ripple. To achieve
204 // this, we re-map the coordinates of the item's InteractionSource into the coordinates of
205 // the indicator.
206 val deltaOffset: Offset
207 with(LocalDensity.current) {
208 val indicatorWidth = NavigationBarTokens.ActiveIndicatorWidth.roundToPx()
209 deltaOffset = Offset(
210 (itemWidth - indicatorWidth).toFloat() / 2,
211 IndicatorVerticalOffset.toPx()
212 )
213 }
214 val offsetInteractionSource = remember(interactionSource, deltaOffset) {
215 MappedInteractionSource(interactionSource, deltaOffset)
216 }
217
218 // The indicator has a width-expansion animation which interferes with the timing of the
219 // ripple, which is why they are separate composables
220 val indicatorRipple = @Composable {
221 Box(
222 Modifier.layoutId(IndicatorRippleLayoutIdTag)
223 .clip(NavigationBarTokens.ActiveIndicatorShape.toShape())
224 .indication(offsetInteractionSource, rememberRipple())
225 )
226 }
Max Yingcc44d972021-10-11 13:12:45 -0400227 val indicator = @Composable {
228 Box(
229 Modifier.layoutId(IndicatorLayoutIdTag)
230 .background(
231 color = colors.indicatorColor.copy(alpha = animationProgress),
Jose Alba Aguado32056032022-03-22 12:50:19 +0100232 shape = NavigationBarTokens.ActiveIndicatorShape.toShape(),
Max Yingcc44d972021-10-11 13:12:45 -0400233 )
234 )
235 }
236
237 NavigationBarItemBaselineLayout(
Max Ying51d3d9e2022-04-08 17:59:07 +0000238 indicatorRipple = indicatorRipple,
Max Yingcc44d972021-10-11 13:12:45 -0400239 indicator = indicator,
240 icon = styledIcon,
241 label = styledLabel,
242 alwaysShowLabel = alwaysShowLabel,
243 animationProgress = animationProgress
244 )
245 }
246}
247
José Figueroa9da8cec2022-05-26 14:59:39 -0400248/** Defaults used in [NavigationBar]. */
249object NavigationBarDefaults {
José Figueroa9da8cec2022-05-26 14:59:39 -0400250 /** Default elevation for a navigation bar. */
251 val Elevation: Dp = NavigationBarTokens.ContainerElevation
José Figueroa Santos49aff522022-07-19 14:10:38 -0400252
253 /** Default color for a navigation bar. */
254 val containerColor: Color @Composable get() = NavigationBarTokens.ContainerColor.toColor()
José Figueroa9da8cec2022-05-26 14:59:39 -0400255}
256
Max Yingcc44d972021-10-11 13:12:45 -0400257/** Defaults used in [NavigationBarItem]. */
258object NavigationBarItemDefaults {
José Figueroa9da8cec2022-05-26 14:59:39 -0400259
Max Yingcc44d972021-10-11 13:12:45 -0400260 /**
261 * Creates a [NavigationBarItemColors] with the provided colors according to the Material
262 * specification.
263 *
264 * @param selectedIconColor the color to use for the icon when the item is selected.
Max Yingcc44d972021-10-11 13:12:45 -0400265 * @param selectedTextColor the color to use for the text label when the item is selected.
Max Yingcc44d972021-10-11 13:12:45 -0400266 * @param indicatorColor the color to use for the indicator when the item is selected.
Connie Shi9d1b1ad2022-07-28 14:33:05 -0400267 * @param unselectedIconColor the color to use for the icon when the item is unselected.
268 * @param unselectedTextColor the color to use for the text label when the item is unselected.
Max Yingcc44d972021-10-11 13:12:45 -0400269 * @return the resulting [NavigationBarItemColors] used for [NavigationBarItem]
270 */
271 @Composable
272 fun colors(
Mariano15a489b2022-01-19 13:08:12 -0500273 selectedIconColor: Color = NavigationBarTokens.ActiveIconColor.toColor(),
Mariano15a489b2022-01-19 13:08:12 -0500274 selectedTextColor: Color = NavigationBarTokens.ActiveLabelTextColor.toColor(),
Mariano15a489b2022-01-19 13:08:12 -0500275 indicatorColor: Color = NavigationBarTokens.ActiveIndicatorColor.toColor(),
Connie Shi9d1b1ad2022-07-28 14:33:05 -0400276 unselectedIconColor: Color = NavigationBarTokens.InactiveIconColor.toColor(),
277 unselectedTextColor: Color = NavigationBarTokens.InactiveLabelTextColor.toColor(),
Max Yingcc44d972021-10-11 13:12:45 -0400278 ): NavigationBarItemColors = remember(
279 selectedIconColor,
280 unselectedIconColor,
281 selectedTextColor,
282 unselectedTextColor,
283 indicatorColor
284 ) {
285 DefaultNavigationBarItemColors(
286 selectedIconColor = selectedIconColor,
Max Yingcc44d972021-10-11 13:12:45 -0400287 selectedTextColor = selectedTextColor,
Max Yingcc44d972021-10-11 13:12:45 -0400288 selectedIndicatorColor = indicatorColor,
Connie Shi9d1b1ad2022-07-28 14:33:05 -0400289 unselectedIconColor = unselectedIconColor,
290 unselectedTextColor = unselectedTextColor,
Max Yingcc44d972021-10-11 13:12:45 -0400291 )
292 }
293}
294
295/** Represents the colors of the various elements of a navigation item. */
296@Stable
297interface NavigationBarItemColors {
298 /**
299 * Represents the icon color for this item, depending on whether it is [selected].
300 *
301 * @param selected whether the item is selected
302 */
303 @Composable
304 fun iconColor(selected: Boolean): State<Color>
305
306 /**
307 * Represents the text color for this item, depending on whether it is [selected].
308 *
309 * @param selected whether the item is selected
310 */
311 @Composable
312 fun textColor(selected: Boolean): State<Color>
313
314 /** Represents the color of the indicator used for selected items. */
315 val indicatorColor: Color
316 @Composable get
317}
318
319@Stable
320private class DefaultNavigationBarItemColors(
321 private val selectedIconColor: Color,
Max Yingcc44d972021-10-11 13:12:45 -0400322 private val selectedTextColor: Color,
Max Yingcc44d972021-10-11 13:12:45 -0400323 private val selectedIndicatorColor: Color,
Connie Shi9d1b1ad2022-07-28 14:33:05 -0400324 private val unselectedIconColor: Color,
325 private val unselectedTextColor: Color,
Max Yingcc44d972021-10-11 13:12:45 -0400326) : NavigationBarItemColors {
327 @Composable
328 override fun iconColor(selected: Boolean): State<Color> {
329 return animateColorAsState(
330 targetValue = if (selected) selectedIconColor else unselectedIconColor,
331 animationSpec = tween(ItemAnimationDurationMillis)
332 )
333 }
334
335 @Composable
336 override fun textColor(selected: Boolean): State<Color> {
337 return animateColorAsState(
338 targetValue = if (selected) selectedTextColor else unselectedTextColor,
339 animationSpec = tween(ItemAnimationDurationMillis)
340 )
341 }
342
343 override val indicatorColor: Color
344 @Composable
345 get() = selectedIndicatorColor
346}
347
348/**
349 * Base layout for a [NavigationBarItem].
350 *
Max Ying51d3d9e2022-04-08 17:59:07 +0000351 * @param indicatorRipple indicator ripple for this item when it is selected
Max Yingcc44d972021-10-11 13:12:45 -0400352 * @param indicator indicator for this item when it is selected
353 * @param icon icon for this item
354 * @param label text label for this item
355 * @param alwaysShowLabel whether to always show the label for this item. If false, the label will
356 * only be shown when this item is selected.
357 * @param animationProgress progress of the animation, where 0 represents the unselected state of
358 * this item and 1 represents the selected state. This value controls other values such as indicator
359 * size, icon and label positions, etc.
360 */
361@Composable
362private fun NavigationBarItemBaselineLayout(
Max Ying51d3d9e2022-04-08 17:59:07 +0000363 indicatorRipple: @Composable () -> Unit,
Max Yingcc44d972021-10-11 13:12:45 -0400364 indicator: @Composable () -> Unit,
365 icon: @Composable () -> Unit,
366 label: @Composable (() -> Unit)?,
367 alwaysShowLabel: Boolean,
368 animationProgress: Float,
369) {
370 Layout({
Max Ying51d3d9e2022-04-08 17:59:07 +0000371 indicatorRipple()
Max Yingcc44d972021-10-11 13:12:45 -0400372 if (animationProgress > 0) {
373 indicator()
374 }
375
376 Box(Modifier.layoutId(IconLayoutIdTag)) { icon() }
377
378 if (label != null) {
379 Box(
380 Modifier.layoutId(LabelLayoutIdTag)
381 .alpha(if (alwaysShowLabel) 1f else animationProgress)
Max Ying51d3d9e2022-04-08 17:59:07 +0000382 .padding(horizontal = NavigationBarItemHorizontalPadding / 2)
Max Yingcc44d972021-10-11 13:12:45 -0400383 ) { label() }
384 }
385 }) { measurables, constraints ->
386 val iconPlaceable =
387 measurables.first { it.layoutId == IconLayoutIdTag }.measure(constraints)
388
389 val totalIndicatorWidth = iconPlaceable.width + (IndicatorHorizontalPadding * 2).roundToPx()
390 val animatedIndicatorWidth = (totalIndicatorWidth * animationProgress).roundToInt()
391 val indicatorHeight = iconPlaceable.height + (IndicatorVerticalPadding * 2).roundToPx()
Max Ying51d3d9e2022-04-08 17:59:07 +0000392 val indicatorRipplePlaceable =
393 measurables
394 .first { it.layoutId == IndicatorRippleLayoutIdTag }
395 .measure(
396 Constraints.fixed(
397 width = totalIndicatorWidth,
398 height = indicatorHeight
399 )
400 )
Max Yingcc44d972021-10-11 13:12:45 -0400401 val indicatorPlaceable =
402 measurables
403 .firstOrNull { it.layoutId == IndicatorLayoutIdTag }
404 ?.measure(
405 Constraints.fixed(
406 width = animatedIndicatorWidth,
407 height = indicatorHeight
408 )
409 )
410
411 val labelPlaceable =
412 label?.let {
413 measurables
414 .first { it.layoutId == LabelLayoutIdTag }
415 .measure(
Max Ying2384bc82022-04-28 20:20:58 +0000416 // Measure with loose constraints for height as we don't want the label to
417 // take up more space than it needs
Max Yingcc44d972021-10-11 13:12:45 -0400418 constraints.copy(minHeight = 0)
419 )
420 }
421
422 if (label == null) {
Max Ying51d3d9e2022-04-08 17:59:07 +0000423 placeIcon(iconPlaceable, indicatorRipplePlaceable, indicatorPlaceable, constraints)
Max Yingcc44d972021-10-11 13:12:45 -0400424 } else {
425 placeLabelAndIcon(
426 labelPlaceable!!,
427 iconPlaceable,
Max Ying51d3d9e2022-04-08 17:59:07 +0000428 indicatorRipplePlaceable,
Max Yingcc44d972021-10-11 13:12:45 -0400429 indicatorPlaceable,
430 constraints,
431 alwaysShowLabel,
432 animationProgress
433 )
434 }
435 }
436}
437
438/**
Max Ying51d3d9e2022-04-08 17:59:07 +0000439 * Places the provided [Placeable]s in the center of the provided [constraints].
Max Yingcc44d972021-10-11 13:12:45 -0400440 */
441private fun MeasureScope.placeIcon(
442 iconPlaceable: Placeable,
Max Ying51d3d9e2022-04-08 17:59:07 +0000443 indicatorRipplePlaceable: Placeable,
Max Yingcc44d972021-10-11 13:12:45 -0400444 indicatorPlaceable: Placeable?,
445 constraints: Constraints
446): MeasureResult {
447 val width = constraints.maxWidth
448 val height = constraints.maxHeight
449
450 val iconX = (width - iconPlaceable.width) / 2
451 val iconY = (height - iconPlaceable.height) / 2
452
Max Ying51d3d9e2022-04-08 17:59:07 +0000453 val rippleX = (width - indicatorRipplePlaceable.width) / 2
454 val rippleY = (height - indicatorRipplePlaceable.height) / 2
455
Max Yingcc44d972021-10-11 13:12:45 -0400456 return layout(width, height) {
457 indicatorPlaceable?.let {
458 val indicatorX = (width - it.width) / 2
459 val indicatorY = (height - it.height) / 2
460 it.placeRelative(indicatorX, indicatorY)
461 }
462 iconPlaceable.placeRelative(iconX, iconY)
Max Ying51d3d9e2022-04-08 17:59:07 +0000463 indicatorRipplePlaceable.placeRelative(rippleX, rippleY)
Max Yingcc44d972021-10-11 13:12:45 -0400464 }
465}
466
467/**
Max Ying51d3d9e2022-04-08 17:59:07 +0000468 * Places the provided [Placeable]s in the correct position, depending on [alwaysShowLabel] and
469 * [animationProgress].
Max Yingcc44d972021-10-11 13:12:45 -0400470 *
471 * When [alwaysShowLabel] is true, the positions do not move. The [iconPlaceable] will be placed
472 * near the top of the item and the [labelPlaceable] will be placed near the bottom, according to
473 * the spec.
474 *
475 * When [animationProgress] is 1 (representing the selected state), the positions will be the same
476 * as above.
477 *
478 * Otherwise, when [animationProgress] is 0, [iconPlaceable] will be placed in the center, like in
479 * [placeIcon], and [labelPlaceable] will not be shown.
480 *
481 * When [animationProgress] is animating between these values, [iconPlaceable] and [labelPlaceable]
482 * will be placed at a corresponding interpolated position.
483 *
Max Ying51d3d9e2022-04-08 17:59:07 +0000484 * [indicatorRipplePlaceable] and [indicatorPlaceable] will always be placed in such a way that to
485 * share the same center as [iconPlaceable].
Max Yingcc44d972021-10-11 13:12:45 -0400486 *
487 * @param labelPlaceable text label placeable inside this item
488 * @param iconPlaceable icon placeable inside this item
Max Ying51d3d9e2022-04-08 17:59:07 +0000489 * @param indicatorRipplePlaceable indicator ripple placeable inside this item
Max Yingcc44d972021-10-11 13:12:45 -0400490 * @param indicatorPlaceable indicator placeable inside this item, if it exists
491 * @param constraints constraints of the item
492 * @param alwaysShowLabel whether to always show the label for this item. If true, icon and label
493 * positions will not change. If false, positions transition between 'centered icon with no label'
494 * and 'top aligned icon with label'.
495 * @param animationProgress progress of the animation, where 0 represents the unselected state of
496 * this item and 1 represents the selected state. Values between 0 and 1 interpolate positions of
497 * the icon and label.
498 */
499private fun MeasureScope.placeLabelAndIcon(
500 labelPlaceable: Placeable,
501 iconPlaceable: Placeable,
Max Ying51d3d9e2022-04-08 17:59:07 +0000502 indicatorRipplePlaceable: Placeable,
Max Yingcc44d972021-10-11 13:12:45 -0400503 indicatorPlaceable: Placeable?,
504 constraints: Constraints,
505 alwaysShowLabel: Boolean,
506 animationProgress: Float,
507): MeasureResult {
508 val height = constraints.maxHeight
509
Max Yingcc44d972021-10-11 13:12:45 -0400510 // Label should be `ItemVerticalPadding` from the bottom
Max Ying054270d2022-06-28 15:18:44 +0000511 val labelY = height - labelPlaceable.height - NavigationBarItemVerticalPadding.roundToPx()
Max Yingcc44d972021-10-11 13:12:45 -0400512
513 // Icon (when selected) should be `ItemVerticalPadding` from the top
514 val selectedIconY = NavigationBarItemVerticalPadding.roundToPx()
515 val unselectedIconY =
516 if (alwaysShowLabel) selectedIconY else (height - iconPlaceable.height) / 2
517
518 // How far the icon needs to move between unselected and selected states.
519 val iconDistance = unselectedIconY - selectedIconY
520
521 // The interpolated fraction of iconDistance that all placeables need to move based on
522 // animationProgress.
523 val offset = (iconDistance * (1 - animationProgress)).roundToInt()
524
525 val containerWidth = constraints.maxWidth
526
527 val labelX = (containerWidth - labelPlaceable.width) / 2
528 val iconX = (containerWidth - iconPlaceable.width) / 2
529
Max Ying51d3d9e2022-04-08 17:59:07 +0000530 val rippleX = (containerWidth - indicatorRipplePlaceable.width) / 2
531 val rippleY = selectedIconY - IndicatorVerticalPadding.roundToPx()
532
Max Yingcc44d972021-10-11 13:12:45 -0400533 return layout(containerWidth, height) {
534 indicatorPlaceable?.let {
535 val indicatorX = (containerWidth - it.width) / 2
536 val indicatorY = selectedIconY - IndicatorVerticalPadding.roundToPx()
537 it.placeRelative(indicatorX, indicatorY + offset)
538 }
539 if (alwaysShowLabel || animationProgress != 0f) {
540 labelPlaceable.placeRelative(labelX, labelY + offset)
541 }
542 iconPlaceable.placeRelative(iconX, selectedIconY + offset)
Max Ying51d3d9e2022-04-08 17:59:07 +0000543 indicatorRipplePlaceable.placeRelative(rippleX, rippleY + offset)
Max Yingcc44d972021-10-11 13:12:45 -0400544 }
545}
546
Max Ying51d3d9e2022-04-08 17:59:07 +0000547private const val IndicatorRippleLayoutIdTag: String = "indicatorRipple"
548
Max Yingcc44d972021-10-11 13:12:45 -0400549private const val IndicatorLayoutIdTag: String = "indicator"
550
551private const val IconLayoutIdTag: String = "icon"
552
553private const val LabelLayoutIdTag: String = "label"
554
Jose Alba Aguadofe2421c2021-11-02 17:37:45 +0100555private val NavigationBarHeight: Dp = NavigationBarTokens.ContainerHeight
Max Yingcc44d972021-10-11 13:12:45 -0400556
557private const val ItemAnimationDurationMillis: Int = 100
558
Max Ying51d3d9e2022-04-08 17:59:07 +0000559/*@VisibleForTesting*/
560internal val NavigationBarItemHorizontalPadding: Dp = 8.dp
Max Yingcc44d972021-10-11 13:12:45 -0400561
562/*@VisibleForTesting*/
563internal val NavigationBarItemVerticalPadding: Dp = 16.dp
564
Max Yingcc44d972021-10-11 13:12:45 -0400565private val IndicatorHorizontalPadding: Dp =
Jose Alba Aguadofe2421c2021-11-02 17:37:45 +0100566 (NavigationBarTokens.ActiveIndicatorWidth - NavigationBarTokens.IconSize) / 2
Max Yingcc44d972021-10-11 13:12:45 -0400567
568private val IndicatorVerticalPadding: Dp =
Max Ying51d3d9e2022-04-08 17:59:07 +0000569 (NavigationBarTokens.ActiveIndicatorHeight - NavigationBarTokens.IconSize) / 2
570
571private val IndicatorVerticalOffset: Dp = 12.dp