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