1 /*
<lambda>null2  * Copyright 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.material3
18 
19 import androidx.compose.animation.animateColorAsState
20 import androidx.compose.animation.core.animateFloatAsState
21 import androidx.compose.foundation.background
22 import androidx.compose.foundation.indication
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.WindowInsets
30 import androidx.compose.foundation.layout.WindowInsetsSides
31 import androidx.compose.foundation.layout.defaultMinSize
32 import androidx.compose.foundation.layout.fillMaxWidth
33 import androidx.compose.foundation.layout.only
34 import androidx.compose.foundation.layout.windowInsetsPadding
35 import androidx.compose.foundation.selection.selectable
36 import androidx.compose.foundation.selection.selectableGroup
37 import androidx.compose.material3.internal.MappedInteractionSource
38 import androidx.compose.material3.internal.ProvideContentColorTextStyle
39 import androidx.compose.material3.internal.systemBarsForVisualComponents
40 import androidx.compose.material3.tokens.ElevationTokens
41 import androidx.compose.material3.tokens.MotionSchemeKeyTokens
42 import androidx.compose.material3.tokens.NavigationBarTokens
43 import androidx.compose.material3.tokens.NavigationBarVerticalItemTokens
44 import androidx.compose.runtime.Composable
45 import androidx.compose.runtime.CompositionLocalProvider
46 import androidx.compose.runtime.Immutable
47 import androidx.compose.runtime.ProvidableCompositionLocal
48 import androidx.compose.runtime.Stable
49 import androidx.compose.runtime.State
50 import androidx.compose.runtime.compositionLocalOf
51 import androidx.compose.runtime.getValue
52 import androidx.compose.runtime.mutableIntStateOf
53 import androidx.compose.runtime.remember
54 import androidx.compose.runtime.setValue
55 import androidx.compose.ui.Alignment
56 import androidx.compose.ui.Modifier
57 import androidx.compose.ui.draw.clip
58 import androidx.compose.ui.geometry.Offset
59 import androidx.compose.ui.graphics.Color
60 import androidx.compose.ui.graphics.graphicsLayer
61 import androidx.compose.ui.graphics.takeOrElse
62 import androidx.compose.ui.layout.Layout
63 import androidx.compose.ui.layout.MeasureResult
64 import androidx.compose.ui.layout.MeasureScope
65 import androidx.compose.ui.layout.Placeable
66 import androidx.compose.ui.layout.layoutId
67 import androidx.compose.ui.layout.onSizeChanged
68 import androidx.compose.ui.platform.LocalDensity
69 import androidx.compose.ui.semantics.Role
70 import androidx.compose.ui.semantics.clearAndSetSemantics
71 import androidx.compose.ui.unit.Constraints
72 import androidx.compose.ui.unit.Dp
73 import androidx.compose.ui.unit.constrainHeight
74 import androidx.compose.ui.unit.dp
75 import androidx.compose.ui.util.fastFirst
76 import androidx.compose.ui.util.fastFirstOrNull
77 import kotlin.math.roundToInt
78 
79 /**
80  * [Material Design bottom navigation
81  * bar](https://m3.material.io/components/navigation-bar/overview)
82  *
83  * Navigation bars offer a persistent and convenient way to switch between primary destinations in
84  * an app.
85  *
86  * ![Navigation bar
87  * image](https://developer.android.com/images/reference/androidx/compose/material3/navigation-bar.png)
88  *
89  * [NavigationBar] should contain three to five [NavigationBarItem]s, each representing a singular
90  * destination.
91  *
92  * A simple example looks like:
93  *
94  * @sample androidx.compose.material3.samples.NavigationBarSample
95  *
96  * See [NavigationBarItem] for configuration specific to each item, and not the overall
97  * [NavigationBar] component.
98  *
99  * @param modifier the [Modifier] to be applied to this navigation bar
100  * @param containerColor the color used for the background of this navigation bar. Use
101  *   [Color.Transparent] to have no color.
102  * @param contentColor the preferred color for content inside this navigation bar. Defaults to
103  *   either the matching content color for [containerColor], or to the current [LocalContentColor]
104  *   if [containerColor] is not a color from the theme.
105  * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
106  *   overlay is applied on top of the container. A higher tonal elevation value will result in a
107  *   darker color in light theme and lighter color in dark theme. See also: [Surface].
108  * @param windowInsets a window insets of the navigation bar.
109  * @param content the content of this navigation bar, typically 3-5 [NavigationBarItem]s
110  */
111 @OptIn(ExperimentalMaterial3ComponentOverrideApi::class)
112 @Composable
113 fun NavigationBar(
114     modifier: Modifier = Modifier,
115     containerColor: Color = NavigationBarDefaults.containerColor,
116     contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor),
117     tonalElevation: Dp = NavigationBarDefaults.Elevation,
118     windowInsets: WindowInsets = NavigationBarDefaults.windowInsets,
119     content: @Composable RowScope.() -> Unit
120 ) {
121     with(LocalNavigationBarOverride.current) {
122         NavigationBarOverrideScope(
123                 modifier = modifier,
124                 containerColor = containerColor,
125                 contentColor = contentColor,
126                 tonalElevation = tonalElevation,
127                 windowInsets = windowInsets,
128                 content = content,
129             )
130             .NavigationBar()
131     }
132 }
133 
134 /**
135  * This override provides the default behavior of the [NavigationBar] component.
136  *
137  * [NavigationBarOverride] used when no override is specified.
138  */
139 @ExperimentalMaterial3ComponentOverrideApi
140 object DefaultNavigationBarOverride : NavigationBarOverride {
141     @Composable
NavigationBarnull142     override fun NavigationBarOverrideScope.NavigationBar() {
143         Surface(
144             color = containerColor,
145             contentColor = contentColor,
146             tonalElevation = tonalElevation,
147             modifier = modifier
148         ) {
149             Row(
150                 modifier =
151                     Modifier.fillMaxWidth()
152                         .windowInsetsPadding(windowInsets)
153                         .defaultMinSize(minHeight = NavigationBarHeight)
154                         .selectableGroup(),
155                 horizontalArrangement = Arrangement.spacedBy(NavigationBarItemHorizontalPadding),
156                 verticalAlignment = Alignment.CenterVertically,
157                 content = content
158             )
159         }
160     }
161 }
162 
163 /**
164  * Material Design navigation bar item.
165  *
166  * Navigation bars offer a persistent and convenient way to switch between primary destinations in
167  * an app.
168  *
169  * The recommended configuration for a [NavigationBarItem] depends on how many items there are
170  * inside a [NavigationBar]:
171  * - Three destinations: Display icons and text labels for all destinations.
172  * - Four destinations: Active destinations display an icon and text label. Inactive destinations
173  *   display icons, and text labels are recommended.
174  * - Five destinations: Active destinations display an icon and text label. Inactive destinations
175  *   use icons, and use text labels if space permits.
176  *
177  * A [NavigationBarItem] always shows text labels (if it exists) when selected. Showing text labels
178  * if not selected is controlled by [alwaysShowLabel].
179  *
180  * @param selected whether this item is selected
181  * @param onClick called when this item is clicked
182  * @param icon icon for this item, typically an [Icon]
183  * @param modifier the [Modifier] to be applied to this item
184  * @param enabled controls the enabled state of this item. When `false`, this component will not
185  *   respond to user input, and it will appear visually disabled and disabled to accessibility
186  *   services.
187  * @param label optional text label for this item
188  * @param alwaysShowLabel whether to always show the label for this item. If `false`, the label will
189  *   only be shown when this item is selected.
190  * @param colors [NavigationBarItemColors] that will be used to resolve the colors used for this
191  *   item in different states. See [NavigationBarItemDefaults.colors].
192  * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
193  *   emitting [Interaction]s for this item. You can use this to change the item's appearance or
194  *   preview the item in different states. Note that if `null` is provided, interactions will still
195  *   happen internally.
196  */
197 @Composable
RowScopenull198 fun RowScope.NavigationBarItem(
199     selected: Boolean,
200     onClick: () -> Unit,
201     icon: @Composable () -> Unit,
202     modifier: Modifier = Modifier,
203     enabled: Boolean = true,
204     label: @Composable (() -> Unit)? = null,
205     alwaysShowLabel: Boolean = true,
206     colors: NavigationBarItemColors = NavigationBarItemDefaults.colors(),
207     interactionSource: MutableInteractionSource? = null
208 ) {
209     @Suppress("NAME_SHADOWING")
210     val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
211     // TODO Load the motionScheme tokens from the component tokens file
212     val colorAnimationSpec = MotionSchemeKeyTokens.DefaultEffects.value<Color>()
213     val styledIcon =
214         @Composable {
215             val iconColor by
216                 animateColorAsState(
217                     targetValue = colors.iconColor(selected = selected, enabled = enabled),
218                     animationSpec = colorAnimationSpec
219                 )
220             // If there's a label, don't have a11y services repeat the icon description.
221             val clearSemantics = label != null && (alwaysShowLabel || selected)
222             Box(modifier = if (clearSemantics) Modifier.clearAndSetSemantics {} else Modifier) {
223                 CompositionLocalProvider(LocalContentColor provides iconColor, content = icon)
224             }
225         }
226 
227     val styledLabel: @Composable (() -> Unit)? =
228         label?.let {
229             @Composable {
230                 val style = NavigationBarTokens.LabelTextFont.value
231                 val textColor by
232                     animateColorAsState(
233                         targetValue = colors.textColor(selected = selected, enabled = enabled),
234                         animationSpec = colorAnimationSpec
235                     )
236                 ProvideContentColorTextStyle(
237                     contentColor = textColor,
238                     textStyle = style,
239                     content = label
240                 )
241             }
242         }
243 
244     var itemWidth by remember { mutableIntStateOf(0) }
245 
246     Box(
247         modifier
248             .selectable(
249                 selected = selected,
250                 onClick = onClick,
251                 enabled = enabled,
252                 role = Role.Tab,
253                 interactionSource = interactionSource,
254                 indication = null,
255             )
256             .defaultMinSize(minHeight = NavigationBarHeight)
257             .weight(1f)
258             .onSizeChanged { itemWidth = it.width },
259         contentAlignment = Alignment.Center,
260         propagateMinConstraints = true,
261     ) {
262         val alphaAnimationProgress: State<Float> =
263             animateFloatAsState(
264                 targetValue = if (selected) 1f else 0f,
265                 // TODO Load the motionScheme tokens from the component tokens file
266                 animationSpec = MotionSchemeKeyTokens.DefaultEffects.value()
267             )
268         val sizeAnimationProgress: State<Float> =
269             animateFloatAsState(
270                 targetValue = if (selected) 1f else 0f,
271                 // TODO Load the motionScheme tokens from the component tokens file
272                 animationSpec = MotionSchemeKeyTokens.FastSpatial.value()
273             )
274         // The entire item is selectable, but only the indicator pill shows the ripple. To achieve
275         // this, we re-map the coordinates of the item's InteractionSource into the coordinates of
276         // the indicator.
277         val deltaOffset: Offset
278         with(LocalDensity.current) {
279             val indicatorWidth = NavigationBarVerticalItemTokens.ActiveIndicatorWidth.roundToPx()
280             deltaOffset =
281                 Offset((itemWidth - indicatorWidth).toFloat() / 2, IndicatorVerticalOffset.toPx())
282         }
283         val offsetInteractionSource =
284             remember(interactionSource, deltaOffset) {
285                 MappedInteractionSource(interactionSource, deltaOffset)
286             }
287 
288         // The indicator has a width-expansion animation which interferes with the timing of the
289         // ripple, which is why they are separate composables
290         val indicatorRipple =
291             @Composable {
292                 Box(
293                     Modifier.layoutId(IndicatorRippleLayoutIdTag)
294                         .clip(NavigationBarTokens.ItemActiveIndicatorShape.value)
295                         .indication(offsetInteractionSource, ripple())
296                 )
297             }
298         val indicator =
299             @Composable {
300                 Box(
301                     Modifier.layoutId(IndicatorLayoutIdTag)
302                         .graphicsLayer { alpha = alphaAnimationProgress.value }
303                         .background(
304                             color = colors.indicatorColor,
305                             shape = NavigationBarTokens.ItemActiveIndicatorShape.value,
306                         )
307                 )
308             }
309 
310         NavigationBarItemLayout(
311             indicatorRipple = indicatorRipple,
312             indicator = indicator,
313             icon = styledIcon,
314             label = styledLabel,
315             alwaysShowLabel = alwaysShowLabel,
316             alphaAnimationProgress = { alphaAnimationProgress.value },
317             sizeAnimationProgress = { sizeAnimationProgress.value }
318         )
319     }
320 }
321 
322 /** Defaults used in [NavigationBar]. */
323 object NavigationBarDefaults {
324     /** Default elevation for a navigation bar. */
325     val Elevation: Dp = ElevationTokens.Level0
326 
327     /** Default color for a navigation bar. */
328     val containerColor: Color
329         @Composable get() = NavigationBarTokens.ContainerColor.value
330 
331     /** Default window insets to be used and consumed by navigation bar */
332     val windowInsets: WindowInsets
333         @Composable
334         get() =
335             WindowInsets.systemBarsForVisualComponents.only(
336                 WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom
337             )
338 }
339 
340 /** Defaults used in [NavigationBarItem]. */
341 object NavigationBarItemDefaults {
342 
343     /**
344      * Creates a [NavigationBarItemColors] with the provided colors according to the Material
345      * specification.
346      */
colorsnull347     @Composable fun colors() = MaterialTheme.colorScheme.defaultNavigationBarItemColors
348 
349     /**
350      * Creates a [NavigationBarItemColors] with the provided colors according to the Material
351      * specification.
352      *
353      * @param selectedIconColor the color to use for the icon when the item is selected.
354      * @param selectedTextColor the color to use for the text label when the item is selected.
355      * @param indicatorColor the color to use for the indicator when the item is selected.
356      * @param unselectedIconColor the color to use for the icon when the item is unselected.
357      * @param unselectedTextColor the color to use for the text label when the item is unselected.
358      * @param disabledIconColor the color to use for the icon when the item is disabled.
359      * @param disabledTextColor the color to use for the text label when the item is disabled.
360      * @return the resulting [NavigationBarItemColors] used for [NavigationBarItem]
361      */
362     @Composable
363     fun colors(
364         selectedIconColor: Color = Color.Unspecified,
365         selectedTextColor: Color = Color.Unspecified,
366         indicatorColor: Color = Color.Unspecified,
367         unselectedIconColor: Color = Color.Unspecified,
368         unselectedTextColor: Color = Color.Unspecified,
369         disabledIconColor: Color = Color.Unspecified,
370         disabledTextColor: Color = Color.Unspecified,
371     ): NavigationBarItemColors =
372         MaterialTheme.colorScheme.defaultNavigationBarItemColors.copy(
373             selectedIconColor = selectedIconColor,
374             selectedTextColor = selectedTextColor,
375             selectedIndicatorColor = indicatorColor,
376             unselectedIconColor = unselectedIconColor,
377             unselectedTextColor = unselectedTextColor,
378             disabledIconColor = disabledIconColor,
379             disabledTextColor = disabledTextColor,
380         )
381 
382     internal val ColorScheme.defaultNavigationBarItemColors: NavigationBarItemColors
383         get() {
384             return defaultNavigationBarItemColorsCached
385                 ?: NavigationBarItemColors(
386                         selectedIconColor = fromToken(NavigationBarTokens.ItemActiveIconColor),
387                         selectedTextColor = fromToken(NavigationBarTokens.ItemActiveLabelTextColor),
388                         selectedIndicatorColor =
389                             fromToken(NavigationBarTokens.ItemActiveIndicatorColor),
390                         unselectedIconColor = fromToken(NavigationBarTokens.ItemInactiveIconColor),
391                         unselectedTextColor =
392                             fromToken(NavigationBarTokens.ItemInactiveLabelTextColor),
393                         disabledIconColor =
394                             fromToken(NavigationBarTokens.ItemInactiveIconColor)
395                                 .copy(alpha = DisabledAlpha),
396                         disabledTextColor =
397                             fromToken(NavigationBarTokens.ItemInactiveLabelTextColor)
398                                 .copy(alpha = DisabledAlpha),
399                     )
400                     .also { defaultNavigationBarItemColorsCached = it }
401         }
402 
403     @Deprecated(
404         "Use overload with disabledIconColor and disabledTextColor",
405         level = DeprecationLevel.HIDDEN
406     )
407     @Composable
colorsnull408     fun colors(
409         selectedIconColor: Color = NavigationBarTokens.ItemActiveIconColor.value,
410         selectedTextColor: Color = NavigationBarTokens.ItemActiveLabelTextColor.value,
411         indicatorColor: Color = NavigationBarTokens.ItemActiveIndicatorColor.value,
412         unselectedIconColor: Color = NavigationBarTokens.ItemInactiveIconColor.value,
413         unselectedTextColor: Color = NavigationBarTokens.ItemInactiveLabelTextColor.value,
414     ): NavigationBarItemColors =
415         NavigationBarItemColors(
416             selectedIconColor = selectedIconColor,
417             selectedTextColor = selectedTextColor,
418             selectedIndicatorColor = indicatorColor,
419             unselectedIconColor = unselectedIconColor,
420             unselectedTextColor = unselectedTextColor,
421             disabledIconColor = unselectedIconColor.copy(alpha = DisabledAlpha),
422             disabledTextColor = unselectedTextColor.copy(alpha = DisabledAlpha),
423         )
424 }
425 
426 /**
427  * Represents the colors of the various elements of a navigation item.
428  *
429  * @param selectedIconColor the color to use for the icon when the item is selected.
430  * @param selectedTextColor the color to use for the text label when the item is selected.
431  * @param selectedIndicatorColor the color to use for the indicator when the item is selected.
432  * @param unselectedIconColor the color to use for the icon when the item is unselected.
433  * @param unselectedTextColor the color to use for the text label when the item is unselected.
434  * @param disabledIconColor the color to use for the icon when the item is disabled.
435  * @param disabledTextColor the color to use for the text label when the item is disabled.
436  * @constructor create an instance with arbitrary colors.
437  */
438 @Immutable
439 class NavigationBarItemColors
440 constructor(
441     val selectedIconColor: Color,
442     val selectedTextColor: Color,
443     val selectedIndicatorColor: Color,
444     val unselectedIconColor: Color,
445     val unselectedTextColor: Color,
446     val disabledIconColor: Color,
447     val disabledTextColor: Color,
448 ) {
449     /**
450      * Returns a copy of this NavigationBarItemColors, optionally overriding some of the values.
451      * This uses the Color.Unspecified to mean “use the value from the source”
452      */
453     fun copy(
454         selectedIconColor: Color = this.selectedIconColor,
455         selectedTextColor: Color = this.selectedTextColor,
456         selectedIndicatorColor: Color = this.selectedIndicatorColor,
457         unselectedIconColor: Color = this.unselectedIconColor,
458         unselectedTextColor: Color = this.unselectedTextColor,
459         disabledIconColor: Color = this.disabledIconColor,
460         disabledTextColor: Color = this.disabledTextColor,
461     ) =
462         NavigationBarItemColors(
463             selectedIconColor.takeOrElse { this.selectedIconColor },
464             selectedTextColor.takeOrElse { this.selectedTextColor },
465             selectedIndicatorColor.takeOrElse { this.selectedIndicatorColor },
466             unselectedIconColor.takeOrElse { this.unselectedIconColor },
467             unselectedTextColor.takeOrElse { this.unselectedTextColor },
468             disabledIconColor.takeOrElse { this.disabledIconColor },
469             disabledTextColor.takeOrElse { this.disabledTextColor },
470         )
471 
472     /**
473      * Represents the icon color for this item, depending on whether it is [selected].
474      *
475      * @param selected whether the item is selected
476      * @param enabled whether the item is enabled
477      */
478     @Stable
479     internal fun iconColor(selected: Boolean, enabled: Boolean): Color =
480         when {
481             !enabled -> disabledIconColor
482             selected -> selectedIconColor
483             else -> unselectedIconColor
484         }
485 
486     /**
487      * Represents the text color for this item, depending on whether it is [selected].
488      *
489      * @param selected whether the item is selected
490      * @param enabled whether the item is enabled
491      */
492     @Stable
493     internal fun textColor(selected: Boolean, enabled: Boolean): Color =
494         when {
495             !enabled -> disabledTextColor
496             selected -> selectedTextColor
497             else -> unselectedTextColor
498         }
499 
500     /** Represents the color of the indicator used for selected items. */
501     internal val indicatorColor: Color
502         get() = selectedIndicatorColor
503 
504     override fun equals(other: Any?): Boolean {
505         if (this === other) return true
506         if (other == null || other !is NavigationBarItemColors) return false
507 
508         if (selectedIconColor != other.selectedIconColor) return false
509         if (unselectedIconColor != other.unselectedIconColor) return false
510         if (selectedTextColor != other.selectedTextColor) return false
511         if (unselectedTextColor != other.unselectedTextColor) return false
512         if (selectedIndicatorColor != other.selectedIndicatorColor) return false
513         if (disabledIconColor != other.disabledIconColor) return false
514         if (disabledTextColor != other.disabledTextColor) return false
515 
516         return true
517     }
518 
519     override fun hashCode(): Int {
520         var result = selectedIconColor.hashCode()
521         result = 31 * result + unselectedIconColor.hashCode()
522         result = 31 * result + selectedTextColor.hashCode()
523         result = 31 * result + unselectedTextColor.hashCode()
524         result = 31 * result + selectedIndicatorColor.hashCode()
525         result = 31 * result + disabledIconColor.hashCode()
526         result = 31 * result + disabledTextColor.hashCode()
527 
528         return result
529     }
530 }
531 
532 /**
533  * Base layout for a [NavigationBarItem].
534  *
535  * @param indicatorRipple indicator ripple for this item when it is selected
536  * @param indicator indicator for this item when it is selected
537  * @param icon icon for this item
538  * @param label text label for this item
539  * @param alwaysShowLabel whether to always show the label for this item. If false, the label will
540  *   only be shown when this item is selected.
541  * @param alphaAnimationProgress progress of the animation, where 0 represents the unselected state
542  *   of this item and 1 represents the selected state. This value controls the indicator's opacity.
543  * @param sizeAnimationProgress progress of the animation, where 0 represents the unselected state
544  *   of this item and 1 represents the selected state. This value controls other values such as
545  *   indicator size, icon and label positions, etc.
546  */
547 @Composable
NavigationBarItemLayoutnull548 private fun NavigationBarItemLayout(
549     indicatorRipple: @Composable () -> Unit,
550     indicator: @Composable () -> Unit,
551     icon: @Composable () -> Unit,
552     label: @Composable (() -> Unit)?,
553     alwaysShowLabel: Boolean,
554     alphaAnimationProgress: () -> Float,
555     sizeAnimationProgress: () -> Float,
556 ) {
557     Layout(
558         modifier = Modifier.badgeBounds(),
559         content = {
560             indicatorRipple()
561             indicator()
562 
563             Box(Modifier.layoutId(IconLayoutIdTag)) { icon() }
564 
565             if (label != null) {
566                 Box(
567                     Modifier.layoutId(LabelLayoutIdTag).graphicsLayer {
568                         alpha = if (alwaysShowLabel) 1f else alphaAnimationProgress()
569                     }
570                 ) {
571                     label()
572                 }
573             }
574         }
575     ) { measurables, constraints ->
576         @Suppress("NAME_SHADOWING")
577         // Ensure that the progress is >= 0. It may be negative on bouncy springs, for example.
578         val animationProgress = sizeAnimationProgress().coerceAtLeast(0f)
579         val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
580         val iconPlaceable =
581             measurables.fastFirst { it.layoutId == IconLayoutIdTag }.measure(looseConstraints)
582 
583         val totalIndicatorWidth = iconPlaceable.width + (IndicatorHorizontalPadding * 2).roundToPx()
584         val animatedIndicatorWidth = (totalIndicatorWidth * animationProgress).roundToInt()
585         val indicatorHeight = iconPlaceable.height + (IndicatorVerticalPadding * 2).roundToPx()
586         val indicatorRipplePlaceable =
587             measurables
588                 .fastFirst { it.layoutId == IndicatorRippleLayoutIdTag }
589                 .measure(Constraints.fixed(width = totalIndicatorWidth, height = indicatorHeight))
590         val indicatorPlaceable =
591             measurables
592                 .fastFirstOrNull { it.layoutId == IndicatorLayoutIdTag }
593                 ?.measure(
594                     Constraints.fixed(width = animatedIndicatorWidth, height = indicatorHeight)
595                 )
596 
597         val labelPlaceable =
598             label?.let {
599                 measurables.fastFirst { it.layoutId == LabelLayoutIdTag }.measure(looseConstraints)
600             }
601 
602         if (label == null) {
603             placeIcon(iconPlaceable, indicatorRipplePlaceable, indicatorPlaceable, constraints)
604         } else {
605             placeLabelAndIcon(
606                 labelPlaceable!!,
607                 iconPlaceable,
608                 indicatorRipplePlaceable,
609                 indicatorPlaceable,
610                 constraints,
611                 alwaysShowLabel,
612                 animationProgress
613             )
614         }
615     }
616 }
617 
618 /** Places the provided [Placeable]s in the center of the provided [constraints]. */
MeasureScopenull619 private fun MeasureScope.placeIcon(
620     iconPlaceable: Placeable,
621     indicatorRipplePlaceable: Placeable,
622     indicatorPlaceable: Placeable?,
623     constraints: Constraints
624 ): MeasureResult {
625     val width =
626         if (constraints.maxWidth == Constraints.Infinity) {
627             iconPlaceable.width + NavigationBarItemToIconMinimumPadding.roundToPx() * 2
628         } else {
629             constraints.maxWidth
630         }
631     val height = constraints.constrainHeight(NavigationBarHeight.roundToPx())
632 
633     val iconX = (width - iconPlaceable.width) / 2
634     val iconY = (height - iconPlaceable.height) / 2
635 
636     val rippleX = (width - indicatorRipplePlaceable.width) / 2
637     val rippleY = (height - indicatorRipplePlaceable.height) / 2
638 
639     return layout(width, height) {
640         indicatorPlaceable?.let {
641             val indicatorX = (width - it.width) / 2
642             val indicatorY = (height - it.height) / 2
643             it.placeRelative(indicatorX, indicatorY)
644         }
645         iconPlaceable.placeRelative(iconX, iconY)
646         indicatorRipplePlaceable.placeRelative(rippleX, rippleY)
647     }
648 }
649 
650 /**
651  * Places the provided [Placeable]s in the correct position, depending on [alwaysShowLabel] and
652  * [animationProgress].
653  *
654  * When [alwaysShowLabel] is true, the positions do not move. The [iconPlaceable] and
655  * [labelPlaceable] will be placed together in the center with padding between them, according to
656  * the spec.
657  *
658  * When [animationProgress] is 1 (representing the selected state), the positions will be the same
659  * as above.
660  *
661  * Otherwise, when [animationProgress] is 0, [iconPlaceable] will be placed in the center, like in
662  * [placeIcon], and [labelPlaceable] will not be shown.
663  *
664  * When [animationProgress] is animating between these values, [iconPlaceable] and [labelPlaceable]
665  * will be placed at a corresponding interpolated position.
666  *
667  * [indicatorRipplePlaceable] and [indicatorPlaceable] will always be placed in such a way that to
668  * share the same center as [iconPlaceable].
669  *
670  * @param labelPlaceable text label placeable inside this item
671  * @param iconPlaceable icon placeable inside this item
672  * @param indicatorRipplePlaceable indicator ripple placeable inside this item
673  * @param indicatorPlaceable indicator placeable inside this item, if it exists
674  * @param constraints constraints of the item
675  * @param alwaysShowLabel whether to always show the label for this item. If true, icon and label
676  *   positions will not change. If false, positions transition between 'centered icon with no label'
677  *   and 'top aligned icon with label'.
678  * @param animationProgress progress of the animation, where 0 represents the unselected state of
679  *   this item and 1 represents the selected state. Values between 0 and 1 interpolate positions of
680  *   the icon and label.
681  */
placeLabelAndIconnull682 private fun MeasureScope.placeLabelAndIcon(
683     labelPlaceable: Placeable,
684     iconPlaceable: Placeable,
685     indicatorRipplePlaceable: Placeable,
686     indicatorPlaceable: Placeable?,
687     constraints: Constraints,
688     alwaysShowLabel: Boolean,
689     animationProgress: Float,
690 ): MeasureResult {
691     val contentHeight =
692         iconPlaceable.height +
693             IndicatorVerticalPadding.toPx() +
694             NavigationBarIndicatorToLabelPadding.toPx() +
695             labelPlaceable.height
696     val contentVerticalPadding =
697         ((constraints.minHeight - contentHeight) / 2).coerceAtLeast(IndicatorVerticalPadding.toPx())
698     val height = contentHeight + contentVerticalPadding * 2
699 
700     // Icon (when selected) should be `contentVerticalPadding` from top
701     val selectedIconY = contentVerticalPadding
702     val unselectedIconY =
703         if (alwaysShowLabel) selectedIconY else (height - iconPlaceable.height) / 2
704 
705     // How far the icon needs to move between unselected and selected states.
706     val iconDistance = unselectedIconY - selectedIconY
707 
708     // The interpolated fraction of iconDistance that all placeables need to move based on
709     // animationProgress.
710     val offset = iconDistance * (1 - animationProgress)
711 
712     // Label should be fixed padding below icon
713     val labelY =
714         selectedIconY +
715             iconPlaceable.height +
716             IndicatorVerticalPadding.toPx() +
717             NavigationBarIndicatorToLabelPadding.toPx()
718 
719     val containerWidth =
720         if (constraints.maxWidth == Constraints.Infinity) {
721             iconPlaceable.width + NavigationBarItemToIconMinimumPadding.roundToPx() * 2
722         } else {
723             constraints.maxWidth
724         }
725 
726     val labelX = (containerWidth - labelPlaceable.width) / 2
727     val iconX = (containerWidth - iconPlaceable.width) / 2
728 
729     val rippleX = (containerWidth - indicatorRipplePlaceable.width) / 2
730     val rippleY = selectedIconY - IndicatorVerticalPadding.toPx()
731 
732     return layout(containerWidth, height.roundToInt()) {
733         indicatorPlaceable?.let {
734             val indicatorX = (containerWidth - it.width) / 2
735             val indicatorY = selectedIconY - IndicatorVerticalPadding.roundToPx()
736             it.placeRelative(indicatorX, (indicatorY + offset).roundToInt())
737         }
738         if (alwaysShowLabel || animationProgress != 0f) {
739             labelPlaceable.placeRelative(labelX, (labelY + offset).roundToInt())
740         }
741         iconPlaceable.placeRelative(iconX, (selectedIconY + offset).roundToInt())
742         indicatorRipplePlaceable.placeRelative(rippleX, (rippleY + offset).roundToInt())
743     }
744 }
745 
746 private const val IndicatorRippleLayoutIdTag: String = "indicatorRipple"
747 
748 private const val IndicatorLayoutIdTag: String = "indicator"
749 
750 private const val IconLayoutIdTag: String = "icon"
751 
752 private const val LabelLayoutIdTag: String = "label"
753 
754 private val NavigationBarHeight: Dp = NavigationBarTokens.TallContainerHeight
755 
756 /*@VisibleForTesting*/
757 internal val NavigationBarItemHorizontalPadding: Dp = 8.dp
758 
759 /*@VisibleForTesting*/
760 internal val NavigationBarIndicatorToLabelPadding: Dp = 4.dp
761 
762 private val IndicatorHorizontalPadding: Dp =
763     (NavigationBarVerticalItemTokens.ActiveIndicatorWidth -
764         NavigationBarVerticalItemTokens.IconSize) / 2
765 
766 /*@VisibleForTesting*/
767 internal val IndicatorVerticalPadding: Dp =
768     (NavigationBarVerticalItemTokens.ActiveIndicatorHeight -
769         NavigationBarVerticalItemTokens.IconSize) / 2
770 
771 private val IndicatorVerticalOffset: Dp = 12.dp
772 
773 /*@VisibleForTesting*/
774 internal val NavigationBarItemToIconMinimumPadding: Dp = 44.dp
775 
776 /**
777  * Interface that allows libraries to override the behavior of the [NavigationBar] component.
778  *
779  * To override this component, implement the member function of this interface, then provide the
780  * implementation to [LocalNavigationBarOverride] in the Compose hierarchy.
781  */
782 @ExperimentalMaterial3ComponentOverrideApi
783 interface NavigationBarOverride {
784     /** Behavior function that is called by the [NavigationBar] component. */
NavigationBarnull785     @Composable fun NavigationBarOverrideScope.NavigationBar()
786 }
787 
788 /**
789  * Parameters available to [NavigationBar].
790  *
791  * @param modifier the [Modifier] to be applied to this navigation bar
792  * @param containerColor the color used for the background of this navigation bar. Use
793  *   [Color.Transparent] to have no color.
794  * @param contentColor the preferred color for content inside this navigation bar. Defaults to
795  *   either the matching content color for [containerColor], or to the current [LocalContentColor]
796  *   if [containerColor] is not a color from the theme.
797  * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
798  *   overlay is applied on top of the container. A higher tonal elevation value will result in a
799  *   darker color in light theme and lighter color in dark theme. See also: [Surface].
800  * @param windowInsets a window insets of the navigation bar.
801  * @param content the content of this navigation bar, typically 3-5 [NavigationBarItem]s
802  */
803 @ExperimentalMaterial3ComponentOverrideApi
804 class NavigationBarOverrideScope
805 internal constructor(
806     val modifier: Modifier = Modifier,
807     val containerColor: Color,
808     val contentColor: Color,
809     val tonalElevation: Dp,
810     val windowInsets: WindowInsets,
811     val content: @Composable RowScope.() -> Unit,
812 )
813 
814 /** CompositionLocal containing the currently-selected [NavigationBarOverride]. */
815 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
816 @get:ExperimentalMaterial3ComponentOverrideApi
817 @ExperimentalMaterial3ComponentOverrideApi
818 val LocalNavigationBarOverride: ProvidableCompositionLocal<NavigationBarOverride> =
819     compositionLocalOf {
820         DefaultNavigationBarOverride
821     }
822