[go: nahoru, domu]

blob: 42541a197c403dc8b9a3a4eb651fe48f29dd9492 [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
59import androidx.compose.ui.unit.Constraints
60import androidx.compose.ui.unit.Dp
61import androidx.compose.ui.unit.dp
62import kotlin.math.roundToInt
63
64/**
Shalom Giblyf42290e2022-04-12 02:12:48 -070065 * <a href="https://m3.material.io/components/navigation-bar/overview" class="external" target="_blank">Material Design bottom navigation bar</a>.
66 *
67 * Navigation bars offer a persistent and convenient way to switch between primary destinations in
68 * an app.
69 *
Nick Rout2f69d9402021-10-19 16:25:18 +020070 * ![Navigation bar image](https://developer.android.com/images/reference/androidx/compose/material3/navigation-bar.png)
71 *
Max Yingcc44d972021-10-11 13:12:45 -040072 * [NavigationBar] should contain three to five [NavigationBarItem]s, each representing a singular
73 * destination.
74 *
75 * A simple example looks like:
76 * @sample androidx.compose.material3.samples.NavigationBarSample
77 *
78 * See [NavigationBarItem] for configuration specific to each item, and not the overall
79 * [NavigationBar] component.
80 *
Max Ying2384bc82022-04-28 20:20:58 +000081 * @param modifier the [Modifier] to be applied to this navigation bar
82 * @param containerColor the color used for the background of this navigation bar. Use
83 * [Color.Transparent] to have no color.
84 * @param contentColor the preferred color for content inside this navigation bar. Defaults to
85 * either the matching content color for [containerColor], or to the current [LocalContentColor] if
86 * [containerColor] is not a color from the theme.
87 * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
88 * overlay is applied on top of the container. A higher tonal elevation value will result in a
89 * darker color in light theme and lighter color in dark theme. See also: [Surface].
90 * @param content the content of this navigation bar, typically 3-5 [NavigationBarItem]s
Max Yingcc44d972021-10-11 13:12:45 -040091 */
92@Composable
93fun NavigationBar(
94 modifier: Modifier = Modifier,
José Figueroa9da8cec2022-05-26 14:59:39 -040095 containerColor: Color = NavigationBarDefaults.ContainerColor,
Max Yingcc44d972021-10-11 13:12:45 -040096 contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor),
José Figueroa9da8cec2022-05-26 14:59:39 -040097 tonalElevation: Dp = NavigationBarDefaults.Elevation,
Max Yingcc44d972021-10-11 13:12:45 -040098 content: @Composable RowScope.() -> Unit
99) {
100 Surface(
101 color = containerColor,
102 contentColor = contentColor,
103 tonalElevation = tonalElevation,
104 modifier = modifier
105 ) {
106 Row(
107 modifier = Modifier.fillMaxWidth().height(NavigationBarHeight).selectableGroup(),
Max Ying51d3d9e2022-04-08 17:59:07 +0000108 horizontalArrangement = Arrangement.spacedBy(NavigationBarItemHorizontalPadding),
Max Yingcc44d972021-10-11 13:12:45 -0400109 content = content
110 )
111 }
112}
113
114/**
115 * Material Design navigation bar item.
116 *
Shalom Giblyf42290e2022-04-12 02:12:48 -0700117 * Navigation bars offer a persistent and convenient way to switch between primary destinations in
118 * an app.
119 *
Max Yingcc44d972021-10-11 13:12:45 -0400120 * The recommended configuration for a [NavigationBarItem] depends on how many items there are
121 * inside a [NavigationBar]:
122 *
123 * - Three destinations: Display icons and text labels for all destinations.
124 * - Four destinations: Active destinations display an icon and text label. Inactive destinations
125 * display icons, and text labels are recommended.
126 * - Five destinations: Active destinations display an icon and text label. Inactive destinations
127 * use icons, and use text labels if space permits.
128 *
129 * A [NavigationBarItem] always shows text labels (if it exists) when selected. Showing text
130 * labels if not selected is controlled by [alwaysShowLabel].
131 *
132 * @param selected whether this item is selected
Max Ying2384bc82022-04-28 20:20:58 +0000133 * @param onClick called when this item is clicked
134 * @param icon icon for this item, typically an [Icon]
135 * @param modifier the [Modifier] to be applied to this item
136 * @param enabled controls the enabled state of this item. When `false`, this component will not
137 * respond to user input, and it will appear visually disabled and disabled to accessibility
138 * services.
Max Yingcc44d972021-10-11 13:12:45 -0400139 * @param label optional text label for this item
Max Ying2384bc82022-04-28 20:20:58 +0000140 * @param alwaysShowLabel whether to always show the label for this item. If `false`, the label will
Max Yingcc44d972021-10-11 13:12:45 -0400141 * only be shown when this item is selected.
142 * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
Max Ying2384bc82022-04-28 20:20:58 +0000143 * for this item. You can create and pass in your own `remember`ed instance to observe
144 * [Interaction]s and customize the appearance / behavior of this item in different states.
145 * @param colors [NavigationBarItemColors] that will be used to resolve the colors used for this
146 * item in different states. See [NavigationBarItemDefaults.colors].
Max Yingcc44d972021-10-11 13:12:45 -0400147 */
148@Composable
149fun RowScope.NavigationBarItem(
150 selected: Boolean,
151 onClick: () -> Unit,
152 icon: @Composable () -> Unit,
153 modifier: Modifier = Modifier,
154 enabled: Boolean = true,
155 label: @Composable (() -> Unit)? = null,
156 alwaysShowLabel: Boolean = true,
157 interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
158 colors: NavigationBarItemColors = NavigationBarItemDefaults.colors()
159) {
160 val styledIcon = @Composable {
161 val iconColor by colors.iconColor(selected = selected)
162 CompositionLocalProvider(LocalContentColor provides iconColor, content = icon)
163 }
164
165 val styledLabel: @Composable (() -> Unit)? = label?.let {
166 @Composable {
Jose Alba Aguadofe2421c2021-11-02 17:37:45 +0100167 val style = MaterialTheme.typography.fromToken(NavigationBarTokens.LabelTextFont)
Max Yingcc44d972021-10-11 13:12:45 -0400168 val textColor by colors.textColor(selected = selected)
169 CompositionLocalProvider(LocalContentColor provides textColor) {
170 ProvideTextStyle(style, content = label)
171 }
172 }
173 }
174
Max Ying51d3d9e2022-04-08 17:59:07 +0000175 var itemWidth by remember { mutableStateOf(0) }
176
Max Yingcc44d972021-10-11 13:12:45 -0400177 Box(
178 modifier
179 .selectable(
180 selected = selected,
181 onClick = onClick,
182 enabled = enabled,
183 role = Role.Tab,
184 interactionSource = interactionSource,
Max Ying51d3d9e2022-04-08 17:59:07 +0000185 indication = null,
Max Yingcc44d972021-10-11 13:12:45 -0400186 )
Max Ying51d3d9e2022-04-08 17:59:07 +0000187 .weight(1f)
188 .onSizeChanged {
189 itemWidth = it.width
190 },
Max Yingcc44d972021-10-11 13:12:45 -0400191 contentAlignment = Alignment.Center
192 ) {
193 val animationProgress: Float by animateFloatAsState(
194 targetValue = if (selected) 1f else 0f,
195 animationSpec = tween(ItemAnimationDurationMillis)
196 )
197
Max Ying51d3d9e2022-04-08 17:59:07 +0000198 // The entire item is selectable, but only the indicator pill shows the ripple. To achieve
199 // this, we re-map the coordinates of the item's InteractionSource into the coordinates of
200 // the indicator.
201 val deltaOffset: Offset
202 with(LocalDensity.current) {
203 val indicatorWidth = NavigationBarTokens.ActiveIndicatorWidth.roundToPx()
204 deltaOffset = Offset(
205 (itemWidth - indicatorWidth).toFloat() / 2,
206 IndicatorVerticalOffset.toPx()
207 )
208 }
209 val offsetInteractionSource = remember(interactionSource, deltaOffset) {
210 MappedInteractionSource(interactionSource, deltaOffset)
211 }
212
213 // The indicator has a width-expansion animation which interferes with the timing of the
214 // ripple, which is why they are separate composables
215 val indicatorRipple = @Composable {
216 Box(
217 Modifier.layoutId(IndicatorRippleLayoutIdTag)
218 .clip(NavigationBarTokens.ActiveIndicatorShape.toShape())
219 .indication(offsetInteractionSource, rememberRipple())
220 )
221 }
Max Yingcc44d972021-10-11 13:12:45 -0400222 val indicator = @Composable {
223 Box(
224 Modifier.layoutId(IndicatorLayoutIdTag)
225 .background(
226 color = colors.indicatorColor.copy(alpha = animationProgress),
Jose Alba Aguado32056032022-03-22 12:50:19 +0100227 shape = NavigationBarTokens.ActiveIndicatorShape.toShape(),
Max Yingcc44d972021-10-11 13:12:45 -0400228 )
229 )
230 }
231
232 NavigationBarItemBaselineLayout(
Max Ying51d3d9e2022-04-08 17:59:07 +0000233 indicatorRipple = indicatorRipple,
Max Yingcc44d972021-10-11 13:12:45 -0400234 indicator = indicator,
235 icon = styledIcon,
236 label = styledLabel,
237 alwaysShowLabel = alwaysShowLabel,
238 animationProgress = animationProgress
239 )
240 }
241}
242
José Figueroa9da8cec2022-05-26 14:59:39 -0400243/** Defaults used in [NavigationBar]. */
244object NavigationBarDefaults {
245 /** Default color for a navigation bar. */
246 val ContainerColor: Color @Composable get() = NavigationBarTokens.ContainerColor.toColor()
247
248 /** Default elevation for a navigation bar. */
249 val Elevation: Dp = NavigationBarTokens.ContainerElevation
250}
251
Max Yingcc44d972021-10-11 13:12:45 -0400252/** Defaults used in [NavigationBarItem]. */
253object NavigationBarItemDefaults {
José Figueroa9da8cec2022-05-26 14:59:39 -0400254
Max Yingcc44d972021-10-11 13:12:45 -0400255 /**
256 * Creates a [NavigationBarItemColors] with the provided colors according to the Material
257 * specification.
258 *
259 * @param selectedIconColor the color to use for the icon when the item is selected.
260 * @param unselectedIconColor the color to use for the icon when the item is unselected.
261 * @param selectedTextColor the color to use for the text label when the item is selected.
262 * @param unselectedTextColor the color to use for the text label when the item is unselected.
263 * @param indicatorColor the color to use for the indicator when the item is selected.
264 * @return the resulting [NavigationBarItemColors] used for [NavigationBarItem]
265 */
266 @Composable
267 fun colors(
Mariano15a489b2022-01-19 13:08:12 -0500268 selectedIconColor: Color = NavigationBarTokens.ActiveIconColor.toColor(),
269 unselectedIconColor: Color = NavigationBarTokens.InactiveIconColor.toColor(),
270 selectedTextColor: Color = NavigationBarTokens.ActiveLabelTextColor.toColor(),
271 unselectedTextColor: Color = NavigationBarTokens.InactiveLabelTextColor.toColor(),
272 indicatorColor: Color = NavigationBarTokens.ActiveIndicatorColor.toColor(),
Max Yingcc44d972021-10-11 13:12:45 -0400273 ): NavigationBarItemColors = remember(
274 selectedIconColor,
275 unselectedIconColor,
276 selectedTextColor,
277 unselectedTextColor,
278 indicatorColor
279 ) {
280 DefaultNavigationBarItemColors(
281 selectedIconColor = selectedIconColor,
282 unselectedIconColor = unselectedIconColor,
283 selectedTextColor = selectedTextColor,
284 unselectedTextColor = unselectedTextColor,
285 selectedIndicatorColor = indicatorColor,
286 )
287 }
288}
289
290/** Represents the colors of the various elements of a navigation item. */
291@Stable
292interface NavigationBarItemColors {
293 /**
294 * Represents the icon color for this item, depending on whether it is [selected].
295 *
296 * @param selected whether the item is selected
297 */
298 @Composable
299 fun iconColor(selected: Boolean): State<Color>
300
301 /**
302 * Represents the text color for this item, depending on whether it is [selected].
303 *
304 * @param selected whether the item is selected
305 */
306 @Composable
307 fun textColor(selected: Boolean): State<Color>
308
309 /** Represents the color of the indicator used for selected items. */
310 val indicatorColor: Color
311 @Composable get
312}
313
314@Stable
315private class DefaultNavigationBarItemColors(
316 private val selectedIconColor: Color,
317 private val unselectedIconColor: Color,
318 private val selectedTextColor: Color,
319 private val unselectedTextColor: Color,
320 private val selectedIndicatorColor: Color,
321) : NavigationBarItemColors {
322 @Composable
323 override fun iconColor(selected: Boolean): State<Color> {
324 return animateColorAsState(
325 targetValue = if (selected) selectedIconColor else unselectedIconColor,
326 animationSpec = tween(ItemAnimationDurationMillis)
327 )
328 }
329
330 @Composable
331 override fun textColor(selected: Boolean): State<Color> {
332 return animateColorAsState(
333 targetValue = if (selected) selectedTextColor else unselectedTextColor,
334 animationSpec = tween(ItemAnimationDurationMillis)
335 )
336 }
337
338 override val indicatorColor: Color
339 @Composable
340 get() = selectedIndicatorColor
341}
342
343/**
344 * Base layout for a [NavigationBarItem].
345 *
Max Ying51d3d9e2022-04-08 17:59:07 +0000346 * @param indicatorRipple indicator ripple for this item when it is selected
Max Yingcc44d972021-10-11 13:12:45 -0400347 * @param indicator indicator for this item when it is selected
348 * @param icon icon for this item
349 * @param label text label for this item
350 * @param alwaysShowLabel whether to always show the label for this item. If false, the label will
351 * only be shown when this item is selected.
352 * @param animationProgress progress of the animation, where 0 represents the unselected state of
353 * this item and 1 represents the selected state. This value controls other values such as indicator
354 * size, icon and label positions, etc.
355 */
356@Composable
357private fun NavigationBarItemBaselineLayout(
Max Ying51d3d9e2022-04-08 17:59:07 +0000358 indicatorRipple: @Composable () -> Unit,
Max Yingcc44d972021-10-11 13:12:45 -0400359 indicator: @Composable () -> Unit,
360 icon: @Composable () -> Unit,
361 label: @Composable (() -> Unit)?,
362 alwaysShowLabel: Boolean,
363 animationProgress: Float,
364) {
365 Layout({
Max Ying51d3d9e2022-04-08 17:59:07 +0000366 indicatorRipple()
Max Yingcc44d972021-10-11 13:12:45 -0400367 if (animationProgress > 0) {
368 indicator()
369 }
370
371 Box(Modifier.layoutId(IconLayoutIdTag)) { icon() }
372
373 if (label != null) {
374 Box(
375 Modifier.layoutId(LabelLayoutIdTag)
376 .alpha(if (alwaysShowLabel) 1f else animationProgress)
Max Ying51d3d9e2022-04-08 17:59:07 +0000377 .padding(horizontal = NavigationBarItemHorizontalPadding / 2)
Max Yingcc44d972021-10-11 13:12:45 -0400378 ) { label() }
379 }
380 }) { measurables, constraints ->
381 val iconPlaceable =
382 measurables.first { it.layoutId == IconLayoutIdTag }.measure(constraints)
383
384 val totalIndicatorWidth = iconPlaceable.width + (IndicatorHorizontalPadding * 2).roundToPx()
385 val animatedIndicatorWidth = (totalIndicatorWidth * animationProgress).roundToInt()
386 val indicatorHeight = iconPlaceable.height + (IndicatorVerticalPadding * 2).roundToPx()
Max Ying51d3d9e2022-04-08 17:59:07 +0000387 val indicatorRipplePlaceable =
388 measurables
389 .first { it.layoutId == IndicatorRippleLayoutIdTag }
390 .measure(
391 Constraints.fixed(
392 width = totalIndicatorWidth,
393 height = indicatorHeight
394 )
395 )
Max Yingcc44d972021-10-11 13:12:45 -0400396 val indicatorPlaceable =
397 measurables
398 .firstOrNull { it.layoutId == IndicatorLayoutIdTag }
399 ?.measure(
400 Constraints.fixed(
401 width = animatedIndicatorWidth,
402 height = indicatorHeight
403 )
404 )
405
406 val labelPlaceable =
407 label?.let {
408 measurables
409 .first { it.layoutId == LabelLayoutIdTag }
410 .measure(
Max Ying2384bc82022-04-28 20:20:58 +0000411 // Measure with loose constraints for height as we don't want the label to
412 // take up more space than it needs
Max Yingcc44d972021-10-11 13:12:45 -0400413 constraints.copy(minHeight = 0)
414 )
415 }
416
417 if (label == null) {
Max Ying51d3d9e2022-04-08 17:59:07 +0000418 placeIcon(iconPlaceable, indicatorRipplePlaceable, indicatorPlaceable, constraints)
Max Yingcc44d972021-10-11 13:12:45 -0400419 } else {
420 placeLabelAndIcon(
421 labelPlaceable!!,
422 iconPlaceable,
Max Ying51d3d9e2022-04-08 17:59:07 +0000423 indicatorRipplePlaceable,
Max Yingcc44d972021-10-11 13:12:45 -0400424 indicatorPlaceable,
425 constraints,
426 alwaysShowLabel,
427 animationProgress
428 )
429 }
430 }
431}
432
433/**
Max Ying51d3d9e2022-04-08 17:59:07 +0000434 * Places the provided [Placeable]s in the center of the provided [constraints].
Max Yingcc44d972021-10-11 13:12:45 -0400435 */
436private fun MeasureScope.placeIcon(
437 iconPlaceable: Placeable,
Max Ying51d3d9e2022-04-08 17:59:07 +0000438 indicatorRipplePlaceable: Placeable,
Max Yingcc44d972021-10-11 13:12:45 -0400439 indicatorPlaceable: Placeable?,
440 constraints: Constraints
441): MeasureResult {
442 val width = constraints.maxWidth
443 val height = constraints.maxHeight
444
445 val iconX = (width - iconPlaceable.width) / 2
446 val iconY = (height - iconPlaceable.height) / 2
447
Max Ying51d3d9e2022-04-08 17:59:07 +0000448 val rippleX = (width - indicatorRipplePlaceable.width) / 2
449 val rippleY = (height - indicatorRipplePlaceable.height) / 2
450
Max Yingcc44d972021-10-11 13:12:45 -0400451 return layout(width, height) {
452 indicatorPlaceable?.let {
453 val indicatorX = (width - it.width) / 2
454 val indicatorY = (height - it.height) / 2
455 it.placeRelative(indicatorX, indicatorY)
456 }
457 iconPlaceable.placeRelative(iconX, iconY)
Max Ying51d3d9e2022-04-08 17:59:07 +0000458 indicatorRipplePlaceable.placeRelative(rippleX, rippleY)
Max Yingcc44d972021-10-11 13:12:45 -0400459 }
460}
461
462/**
Max Ying51d3d9e2022-04-08 17:59:07 +0000463 * Places the provided [Placeable]s in the correct position, depending on [alwaysShowLabel] and
464 * [animationProgress].
Max Yingcc44d972021-10-11 13:12:45 -0400465 *
466 * When [alwaysShowLabel] is true, the positions do not move. The [iconPlaceable] will be placed
467 * near the top of the item and the [labelPlaceable] will be placed near the bottom, according to
468 * the spec.
469 *
470 * When [animationProgress] is 1 (representing the selected state), the positions will be the same
471 * as above.
472 *
473 * Otherwise, when [animationProgress] is 0, [iconPlaceable] will be placed in the center, like in
474 * [placeIcon], and [labelPlaceable] will not be shown.
475 *
476 * When [animationProgress] is animating between these values, [iconPlaceable] and [labelPlaceable]
477 * will be placed at a corresponding interpolated position.
478 *
Max Ying51d3d9e2022-04-08 17:59:07 +0000479 * [indicatorRipplePlaceable] and [indicatorPlaceable] will always be placed in such a way that to
480 * share the same center as [iconPlaceable].
Max Yingcc44d972021-10-11 13:12:45 -0400481 *
482 * @param labelPlaceable text label placeable inside this item
483 * @param iconPlaceable icon placeable inside this item
Max Ying51d3d9e2022-04-08 17:59:07 +0000484 * @param indicatorRipplePlaceable indicator ripple placeable inside this item
Max Yingcc44d972021-10-11 13:12:45 -0400485 * @param indicatorPlaceable indicator placeable inside this item, if it exists
486 * @param constraints constraints of the item
487 * @param alwaysShowLabel whether to always show the label for this item. If true, icon and label
488 * positions will not change. If false, positions transition between 'centered icon with no label'
489 * and 'top aligned icon with label'.
490 * @param animationProgress progress of the animation, where 0 represents the unselected state of
491 * this item and 1 represents the selected state. Values between 0 and 1 interpolate positions of
492 * the icon and label.
493 */
494private fun MeasureScope.placeLabelAndIcon(
495 labelPlaceable: Placeable,
496 iconPlaceable: Placeable,
Max Ying51d3d9e2022-04-08 17:59:07 +0000497 indicatorRipplePlaceable: Placeable,
Max Yingcc44d972021-10-11 13:12:45 -0400498 indicatorPlaceable: Placeable?,
499 constraints: Constraints,
500 alwaysShowLabel: Boolean,
501 animationProgress: Float,
502): MeasureResult {
503 val height = constraints.maxHeight
504
Max Yingcc44d972021-10-11 13:12:45 -0400505 // Label should be `ItemVerticalPadding` from the bottom
Max Ying054270d2022-06-28 15:18:44 +0000506 val labelY = height - labelPlaceable.height - NavigationBarItemVerticalPadding.roundToPx()
Max Yingcc44d972021-10-11 13:12:45 -0400507
508 // Icon (when selected) should be `ItemVerticalPadding` from the top
509 val selectedIconY = NavigationBarItemVerticalPadding.roundToPx()
510 val unselectedIconY =
511 if (alwaysShowLabel) selectedIconY else (height - iconPlaceable.height) / 2
512
513 // How far the icon needs to move between unselected and selected states.
514 val iconDistance = unselectedIconY - selectedIconY
515
516 // The interpolated fraction of iconDistance that all placeables need to move based on
517 // animationProgress.
518 val offset = (iconDistance * (1 - animationProgress)).roundToInt()
519
520 val containerWidth = constraints.maxWidth
521
522 val labelX = (containerWidth - labelPlaceable.width) / 2
523 val iconX = (containerWidth - iconPlaceable.width) / 2
524
Max Ying51d3d9e2022-04-08 17:59:07 +0000525 val rippleX = (containerWidth - indicatorRipplePlaceable.width) / 2
526 val rippleY = selectedIconY - IndicatorVerticalPadding.roundToPx()
527
Max Yingcc44d972021-10-11 13:12:45 -0400528 return layout(containerWidth, height) {
529 indicatorPlaceable?.let {
530 val indicatorX = (containerWidth - it.width) / 2
531 val indicatorY = selectedIconY - IndicatorVerticalPadding.roundToPx()
532 it.placeRelative(indicatorX, indicatorY + offset)
533 }
534 if (alwaysShowLabel || animationProgress != 0f) {
535 labelPlaceable.placeRelative(labelX, labelY + offset)
536 }
537 iconPlaceable.placeRelative(iconX, selectedIconY + offset)
Max Ying51d3d9e2022-04-08 17:59:07 +0000538 indicatorRipplePlaceable.placeRelative(rippleX, rippleY + offset)
Max Yingcc44d972021-10-11 13:12:45 -0400539 }
540}
541
Max Ying51d3d9e2022-04-08 17:59:07 +0000542private const val IndicatorRippleLayoutIdTag: String = "indicatorRipple"
543
Max Yingcc44d972021-10-11 13:12:45 -0400544private const val IndicatorLayoutIdTag: String = "indicator"
545
546private const val IconLayoutIdTag: String = "icon"
547
548private const val LabelLayoutIdTag: String = "label"
549
Jose Alba Aguadofe2421c2021-11-02 17:37:45 +0100550private val NavigationBarHeight: Dp = NavigationBarTokens.ContainerHeight
Max Yingcc44d972021-10-11 13:12:45 -0400551
552private const val ItemAnimationDurationMillis: Int = 100
553
Max Ying51d3d9e2022-04-08 17:59:07 +0000554/*@VisibleForTesting*/
555internal val NavigationBarItemHorizontalPadding: Dp = 8.dp
Max Yingcc44d972021-10-11 13:12:45 -0400556
557/*@VisibleForTesting*/
558internal val NavigationBarItemVerticalPadding: Dp = 16.dp
559
Max Yingcc44d972021-10-11 13:12:45 -0400560private val IndicatorHorizontalPadding: Dp =
Jose Alba Aguadofe2421c2021-11-02 17:37:45 +0100561 (NavigationBarTokens.ActiveIndicatorWidth - NavigationBarTokens.IconSize) / 2
Max Yingcc44d972021-10-11 13:12:45 -0400562
563private val IndicatorVerticalPadding: Dp =
Max Ying51d3d9e2022-04-08 17:59:07 +0000564 (NavigationBarTokens.ActiveIndicatorHeight - NavigationBarTokens.IconSize) / 2
565
566private val IndicatorVerticalOffset: Dp = 12.dp