[go: nahoru, domu]

blob: 274643674d58cea2421c93b4ab09b03cd78940ef [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
Matvei Malkov848ce532022-08-04 16:56:35 +010030import androidx.compose.foundation.layout.WindowInsets
31import androidx.compose.foundation.layout.WindowInsetsSides
Max Yingcc44d972021-10-11 13:12:45 -040032import androidx.compose.foundation.layout.fillMaxWidth
33import androidx.compose.foundation.layout.height
Matvei Malkov848ce532022-08-04 16:56:35 +010034import androidx.compose.foundation.layout.only
Max Yingcc44d972021-10-11 13:12:45 -040035import androidx.compose.foundation.layout.padding
Matvei Malkov848ce532022-08-04 16:56:35 +010036import androidx.compose.foundation.layout.windowInsetsPadding
Max Yingcc44d972021-10-11 13:12:45 -040037import androidx.compose.foundation.selection.selectable
38import androidx.compose.foundation.selection.selectableGroup
39import androidx.compose.material.ripple.rememberRipple
Jose Alba Aguadofe2421c2021-11-02 17:37:45 +010040import androidx.compose.material3.tokens.NavigationBarTokens
Max Yingcc44d972021-10-11 13:12:45 -040041import androidx.compose.runtime.Composable
42import androidx.compose.runtime.CompositionLocalProvider
43import androidx.compose.runtime.Stable
44import androidx.compose.runtime.State
45import androidx.compose.runtime.getValue
Max Ying51d3d9e2022-04-08 17:59:07 +000046import androidx.compose.runtime.mutableStateOf
Max Yingcc44d972021-10-11 13:12:45 -040047import androidx.compose.runtime.remember
Max Ying51d3d9e2022-04-08 17:59:07 +000048import androidx.compose.runtime.setValue
Max Yingcc44d972021-10-11 13:12:45 -040049import androidx.compose.ui.Alignment
50import androidx.compose.ui.Modifier
51import androidx.compose.ui.draw.alpha
Max Ying51d3d9e2022-04-08 17:59:07 +000052import androidx.compose.ui.draw.clip
53import androidx.compose.ui.geometry.Offset
Max Yingcc44d972021-10-11 13:12:45 -040054import androidx.compose.ui.graphics.Color
Max Yingcc44d972021-10-11 13:12:45 -040055import androidx.compose.ui.layout.Layout
56import androidx.compose.ui.layout.MeasureResult
57import androidx.compose.ui.layout.MeasureScope
58import androidx.compose.ui.layout.Placeable
59import androidx.compose.ui.layout.layoutId
Max Ying51d3d9e2022-04-08 17:59:07 +000060import androidx.compose.ui.layout.onSizeChanged
61import androidx.compose.ui.platform.LocalDensity
Max Yingcc44d972021-10-11 13:12:45 -040062import androidx.compose.ui.semantics.Role
Max Alfonso-Ying35550f62022-07-14 20:37:06 +000063import androidx.compose.ui.semantics.clearAndSetSemantics
Max Yingcc44d972021-10-11 13:12:45 -040064import androidx.compose.ui.unit.Constraints
65import androidx.compose.ui.unit.Dp
66import androidx.compose.ui.unit.dp
67import kotlin.math.roundToInt
68
69/**
Shalom Giblyf42290e2022-04-12 02:12:48 -070070 * <a href="https://m3.material.io/components/navigation-bar/overview" class="external" target="_blank">Material Design bottom navigation bar</a>.
71 *
72 * Navigation bars offer a persistent and convenient way to switch between primary destinations in
73 * an app.
74 *
Nick Rout2f69d9402021-10-19 16:25:18 +020075 * ![Navigation bar image](https://developer.android.com/images/reference/androidx/compose/material3/navigation-bar.png)
76 *
Max Yingcc44d972021-10-11 13:12:45 -040077 * [NavigationBar] should contain three to five [NavigationBarItem]s, each representing a singular
78 * destination.
79 *
80 * A simple example looks like:
81 * @sample androidx.compose.material3.samples.NavigationBarSample
82 *
83 * See [NavigationBarItem] for configuration specific to each item, and not the overall
84 * [NavigationBar] component.
85 *
Max Ying2384bc82022-04-28 20:20:58 +000086 * @param modifier the [Modifier] to be applied to this navigation bar
87 * @param containerColor the color used for the background of this navigation bar. Use
88 * [Color.Transparent] to have no color.
89 * @param contentColor the preferred color for content inside this navigation bar. Defaults to
90 * either the matching content color for [containerColor], or to the current [LocalContentColor] if
91 * [containerColor] is not a color from the theme.
92 * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
93 * overlay is applied on top of the container. A higher tonal elevation value will result in a
94 * darker color in light theme and lighter color in dark theme. See also: [Surface].
Matvei Malkov848ce532022-08-04 16:56:35 +010095 * @param windowInsets a window insets of the navigation bar.
Max Ying2384bc82022-04-28 20:20:58 +000096 * @param content the content of this navigation bar, typically 3-5 [NavigationBarItem]s
Max Yingcc44d972021-10-11 13:12:45 -040097 */
98@Composable
99fun NavigationBar(
100 modifier: Modifier = Modifier,
José Figueroa Santos49aff522022-07-19 14:10:38 -0400101 containerColor: Color = NavigationBarDefaults.containerColor,
Max Yingcc44d972021-10-11 13:12:45 -0400102 contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor),
José Figueroa9da8cec2022-05-26 14:59:39 -0400103 tonalElevation: Dp = NavigationBarDefaults.Elevation,
Matvei Malkov848ce532022-08-04 16:56:35 +0100104 windowInsets: WindowInsets = NavigationBarDefaults.windowInsets,
Max Yingcc44d972021-10-11 13:12:45 -0400105 content: @Composable RowScope.() -> Unit
106) {
107 Surface(
108 color = containerColor,
109 contentColor = contentColor,
110 tonalElevation = tonalElevation,
111 modifier = modifier
112 ) {
113 Row(
Matvei Malkov848ce532022-08-04 16:56:35 +0100114 modifier = Modifier
115 .fillMaxWidth()
116 .windowInsetsPadding(windowInsets)
117 .height(NavigationBarHeight)
118 .selectableGroup(),
Max Ying51d3d9e2022-04-08 17:59:07 +0000119 horizontalArrangement = Arrangement.spacedBy(NavigationBarItemHorizontalPadding),
Max Yingcc44d972021-10-11 13:12:45 -0400120 content = content
121 )
122 }
123}
124
125/**
126 * Material Design navigation bar item.
127 *
Shalom Giblyf42290e2022-04-12 02:12:48 -0700128 * Navigation bars offer a persistent and convenient way to switch between primary destinations in
129 * an app.
130 *
Max Yingcc44d972021-10-11 13:12:45 -0400131 * The recommended configuration for a [NavigationBarItem] depends on how many items there are
132 * inside a [NavigationBar]:
133 *
134 * - Three destinations: Display icons and text labels for all destinations.
135 * - Four destinations: Active destinations display an icon and text label. Inactive destinations
136 * display icons, and text labels are recommended.
137 * - Five destinations: Active destinations display an icon and text label. Inactive destinations
138 * use icons, and use text labels if space permits.
139 *
140 * A [NavigationBarItem] always shows text labels (if it exists) when selected. Showing text
141 * labels if not selected is controlled by [alwaysShowLabel].
142 *
143 * @param selected whether this item is selected
Max Ying2384bc82022-04-28 20:20:58 +0000144 * @param onClick called when this item is clicked
145 * @param icon icon for this item, typically an [Icon]
146 * @param modifier the [Modifier] to be applied to this item
147 * @param enabled controls the enabled state of this item. When `false`, this component will not
148 * respond to user input, and it will appear visually disabled and disabled to accessibility
149 * services.
Max Yingcc44d972021-10-11 13:12:45 -0400150 * @param label optional text label for this item
Max Ying2384bc82022-04-28 20:20:58 +0000151 * @param alwaysShowLabel whether to always show the label for this item. If `false`, the label will
Max Yingcc44d972021-10-11 13:12:45 -0400152 * only be shown when this item is selected.
Connie Shi9d1b1ad2022-07-28 14:33:05 -0400153 * @param colors [NavigationBarItemColors] that will be used to resolve the colors used for this
154 * item in different states. See [NavigationBarItemDefaults.colors].
Max Yingcc44d972021-10-11 13:12:45 -0400155 * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
Max Ying2384bc82022-04-28 20:20:58 +0000156 * for this item. You can create and pass in your own `remember`ed instance to observe
157 * [Interaction]s and customize the appearance / behavior of this item in different states.
Max Yingcc44d972021-10-11 13:12:45 -0400158 */
159@Composable
160fun RowScope.NavigationBarItem(
161 selected: Boolean,
162 onClick: () -> Unit,
163 icon: @Composable () -> Unit,
164 modifier: Modifier = Modifier,
165 enabled: Boolean = true,
166 label: @Composable (() -> Unit)? = null,
167 alwaysShowLabel: Boolean = true,
Connie Shi9d1b1ad2022-07-28 14:33:05 -0400168 colors: NavigationBarItemColors = NavigationBarItemDefaults.colors(),
169 interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
Max Yingcc44d972021-10-11 13:12:45 -0400170) {
171 val styledIcon = @Composable {
Max Alfonso-Ying781cd8a2022-11-14 19:04:31 +0000172 val iconColor by colors.iconColor(selected = selected, enabled = enabled)
Max Alfonso-Ying35550f62022-07-14 20:37:06 +0000173 // If there's a label, don't have a11y services repeat the icon description.
Max Alfonso-Ying9f025b52022-08-01 18:24:55 +0000174 val clearSemantics = label != null && (alwaysShowLabel || selected)
Max Alfonso-Ying35550f62022-07-14 20:37:06 +0000175 Box(modifier = if (clearSemantics) Modifier.clearAndSetSemantics {} else Modifier) {
176 CompositionLocalProvider(LocalContentColor provides iconColor, content = icon)
177 }
Max Yingcc44d972021-10-11 13:12:45 -0400178 }
179
180 val styledLabel: @Composable (() -> Unit)? = label?.let {
181 @Composable {
Jose Alba Aguadofe2421c2021-11-02 17:37:45 +0100182 val style = MaterialTheme.typography.fromToken(NavigationBarTokens.LabelTextFont)
Max Alfonso-Ying781cd8a2022-11-14 19:04:31 +0000183 val textColor by colors.textColor(selected = selected, enabled = enabled)
Max Yingcc44d972021-10-11 13:12:45 -0400184 CompositionLocalProvider(LocalContentColor provides textColor) {
185 ProvideTextStyle(style, content = label)
186 }
187 }
188 }
189
Max Ying51d3d9e2022-04-08 17:59:07 +0000190 var itemWidth by remember { mutableStateOf(0) }
191
Max Yingcc44d972021-10-11 13:12:45 -0400192 Box(
193 modifier
194 .selectable(
195 selected = selected,
196 onClick = onClick,
197 enabled = enabled,
198 role = Role.Tab,
199 interactionSource = interactionSource,
Max Ying51d3d9e2022-04-08 17:59:07 +0000200 indication = null,
Max Yingcc44d972021-10-11 13:12:45 -0400201 )
Max Ying51d3d9e2022-04-08 17:59:07 +0000202 .weight(1f)
203 .onSizeChanged {
204 itemWidth = it.width
205 },
Max Yingcc44d972021-10-11 13:12:45 -0400206 contentAlignment = Alignment.Center
207 ) {
208 val animationProgress: Float by animateFloatAsState(
209 targetValue = if (selected) 1f else 0f,
210 animationSpec = tween(ItemAnimationDurationMillis)
211 )
212
Max Ying51d3d9e2022-04-08 17:59:07 +0000213 // The entire item is selectable, but only the indicator pill shows the ripple. To achieve
214 // this, we re-map the coordinates of the item's InteractionSource into the coordinates of
215 // the indicator.
216 val deltaOffset: Offset
217 with(LocalDensity.current) {
218 val indicatorWidth = NavigationBarTokens.ActiveIndicatorWidth.roundToPx()
219 deltaOffset = Offset(
220 (itemWidth - indicatorWidth).toFloat() / 2,
221 IndicatorVerticalOffset.toPx()
222 )
223 }
224 val offsetInteractionSource = remember(interactionSource, deltaOffset) {
225 MappedInteractionSource(interactionSource, deltaOffset)
226 }
227
228 // The indicator has a width-expansion animation which interferes with the timing of the
229 // ripple, which is why they are separate composables
230 val indicatorRipple = @Composable {
231 Box(
Matvei Malkov848ce532022-08-04 16:56:35 +0100232 Modifier
233 .layoutId(IndicatorRippleLayoutIdTag)
Max Ying51d3d9e2022-04-08 17:59:07 +0000234 .clip(NavigationBarTokens.ActiveIndicatorShape.toShape())
235 .indication(offsetInteractionSource, rememberRipple())
236 )
237 }
Max Yingcc44d972021-10-11 13:12:45 -0400238 val indicator = @Composable {
239 Box(
Matvei Malkov848ce532022-08-04 16:56:35 +0100240 Modifier
241 .layoutId(IndicatorLayoutIdTag)
Max Yingcc44d972021-10-11 13:12:45 -0400242 .background(
243 color = colors.indicatorColor.copy(alpha = animationProgress),
Jose Alba Aguado32056032022-03-22 12:50:19 +0100244 shape = NavigationBarTokens.ActiveIndicatorShape.toShape(),
Max Yingcc44d972021-10-11 13:12:45 -0400245 )
246 )
247 }
248
249 NavigationBarItemBaselineLayout(
Max Ying51d3d9e2022-04-08 17:59:07 +0000250 indicatorRipple = indicatorRipple,
Max Yingcc44d972021-10-11 13:12:45 -0400251 indicator = indicator,
252 icon = styledIcon,
253 label = styledLabel,
254 alwaysShowLabel = alwaysShowLabel,
255 animationProgress = animationProgress
256 )
257 }
258}
259
José Figueroa9da8cec2022-05-26 14:59:39 -0400260/** Defaults used in [NavigationBar]. */
261object NavigationBarDefaults {
José Figueroa9da8cec2022-05-26 14:59:39 -0400262 /** Default elevation for a navigation bar. */
263 val Elevation: Dp = NavigationBarTokens.ContainerElevation
José Figueroa Santos49aff522022-07-19 14:10:38 -0400264
265 /** Default color for a navigation bar. */
266 val containerColor: Color @Composable get() = NavigationBarTokens.ContainerColor.toColor()
Matvei Malkov848ce532022-08-04 16:56:35 +0100267
268 /**
269 * Default window insets to be used and consumed by navigation bar
270 */
271 val windowInsets: WindowInsets
272 @Composable
Matvei Malkov82cc4332022-08-29 18:15:05 -0400273 get() = WindowInsets.systemBarsForVisualComponents
Matvei Malkov848ce532022-08-04 16:56:35 +0100274 .only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom)
José Figueroa9da8cec2022-05-26 14:59:39 -0400275}
276
Max Yingcc44d972021-10-11 13:12:45 -0400277/** Defaults used in [NavigationBarItem]. */
278object NavigationBarItemDefaults {
José Figueroa9da8cec2022-05-26 14:59:39 -0400279
Max Yingcc44d972021-10-11 13:12:45 -0400280 /**
281 * Creates a [NavigationBarItemColors] with the provided colors according to the Material
282 * specification.
283 *
284 * @param selectedIconColor the color to use for the icon when the item is selected.
Max Yingcc44d972021-10-11 13:12:45 -0400285 * @param selectedTextColor the color to use for the text label when the item is selected.
Max Yingcc44d972021-10-11 13:12:45 -0400286 * @param indicatorColor the color to use for the indicator when the item is selected.
Connie Shi9d1b1ad2022-07-28 14:33:05 -0400287 * @param unselectedIconColor the color to use for the icon when the item is unselected.
288 * @param unselectedTextColor the color to use for the text label when the item is unselected.
Max Alfonso-Ying781cd8a2022-11-14 19:04:31 +0000289 * @param disabledIconColor the color to use for the icon when the item is disabled.
290 * @param disabledTextColor the color to use for the text label when the item is disabled.
Max Yingcc44d972021-10-11 13:12:45 -0400291 * @return the resulting [NavigationBarItemColors] used for [NavigationBarItem]
292 */
293 @Composable
294 fun colors(
Mariano15a489b2022-01-19 13:08:12 -0500295 selectedIconColor: Color = NavigationBarTokens.ActiveIconColor.toColor(),
Mariano15a489b2022-01-19 13:08:12 -0500296 selectedTextColor: Color = NavigationBarTokens.ActiveLabelTextColor.toColor(),
Mariano15a489b2022-01-19 13:08:12 -0500297 indicatorColor: Color = NavigationBarTokens.ActiveIndicatorColor.toColor(),
Connie Shi9d1b1ad2022-07-28 14:33:05 -0400298 unselectedIconColor: Color = NavigationBarTokens.InactiveIconColor.toColor(),
299 unselectedTextColor: Color = NavigationBarTokens.InactiveLabelTextColor.toColor(),
Max Alfonso-Ying781cd8a2022-11-14 19:04:31 +0000300 disabledIconColor: Color = unselectedIconColor.copy(alpha = DisabledAlpha),
301 disabledTextColor: Color = unselectedTextColor.copy(alpha = DisabledAlpha),
302 ): NavigationBarItemColors = NavigationBarItemColors(
303 selectedIconColor = selectedIconColor,
304 selectedTextColor = selectedTextColor,
305 selectedIndicatorColor = indicatorColor,
306 unselectedIconColor = unselectedIconColor,
307 unselectedTextColor = unselectedTextColor,
308 disabledIconColor = disabledIconColor,
309 disabledTextColor = disabledTextColor,
310 )
311
312 @Deprecated(
313 "Use overload with disabledIconColor and disabledTextColor",
314 level = DeprecationLevel.HIDDEN
315 )
316 @Composable
317 fun colors(
318 selectedIconColor: Color = NavigationBarTokens.ActiveIconColor.toColor(),
319 selectedTextColor: Color = NavigationBarTokens.ActiveLabelTextColor.toColor(),
320 indicatorColor: Color = NavigationBarTokens.ActiveIndicatorColor.toColor(),
321 unselectedIconColor: Color = NavigationBarTokens.InactiveIconColor.toColor(),
322 unselectedTextColor: Color = NavigationBarTokens.InactiveLabelTextColor.toColor(),
Connie Shi071c4832022-07-12 19:09:26 -0400323 ): NavigationBarItemColors = NavigationBarItemColors(
324 selectedIconColor = selectedIconColor,
325 selectedTextColor = selectedTextColor,
326 selectedIndicatorColor = indicatorColor,
327 unselectedIconColor = unselectedIconColor,
328 unselectedTextColor = unselectedTextColor,
Max Alfonso-Ying781cd8a2022-11-14 19:04:31 +0000329 disabledIconColor = unselectedIconColor.copy(alpha = DisabledAlpha),
330 disabledTextColor = unselectedTextColor.copy(alpha = DisabledAlpha),
Connie Shi071c4832022-07-12 19:09:26 -0400331 )
Max Yingcc44d972021-10-11 13:12:45 -0400332}
333
Max Yingcc44d972021-10-11 13:12:45 -0400334@Stable
Connie Shi071c4832022-07-12 19:09:26 -0400335class NavigationBarItemColors internal constructor(
336 private val selectedIconColor: Color,
337 private val selectedTextColor: Color,
338 private val selectedIndicatorColor: Color,
339 private val unselectedIconColor: Color,
340 private val unselectedTextColor: Color,
Max Alfonso-Ying781cd8a2022-11-14 19:04:31 +0000341 private val disabledIconColor: Color,
342 private val disabledTextColor: Color,
Connie Shi071c4832022-07-12 19:09:26 -0400343) {
Max Yingcc44d972021-10-11 13:12:45 -0400344 /**
345 * Represents the icon color for this item, depending on whether it is [selected].
346 *
347 * @param selected whether the item is selected
Max Alfonso-Ying781cd8a2022-11-14 19:04:31 +0000348 * @param enabled whether the item is enabled
Max Yingcc44d972021-10-11 13:12:45 -0400349 */
350 @Composable
Max Alfonso-Ying781cd8a2022-11-14 19:04:31 +0000351 internal fun iconColor(selected: Boolean, enabled: Boolean): State<Color> {
352 val targetValue = when {
353 !enabled -> disabledIconColor
354 selected -> selectedIconColor
355 else -> unselectedIconColor
356 }
Connie Shi071c4832022-07-12 19:09:26 -0400357 return animateColorAsState(
Max Alfonso-Ying781cd8a2022-11-14 19:04:31 +0000358 targetValue = targetValue,
Connie Shi071c4832022-07-12 19:09:26 -0400359 animationSpec = tween(ItemAnimationDurationMillis)
360 )
361 }
Max Yingcc44d972021-10-11 13:12:45 -0400362
363 /**
364 * Represents the text color for this item, depending on whether it is [selected].
365 *
366 * @param selected whether the item is selected
Max Alfonso-Ying781cd8a2022-11-14 19:04:31 +0000367 * @param enabled whether the item is enabled
Max Yingcc44d972021-10-11 13:12:45 -0400368 */
369 @Composable
Max Alfonso-Ying781cd8a2022-11-14 19:04:31 +0000370 internal fun textColor(selected: Boolean, enabled: Boolean): State<Color> {
371 val targetValue = when {
372 !enabled -> disabledTextColor
373 selected -> selectedTextColor
374 else -> unselectedTextColor
375 }
Max Yingcc44d972021-10-11 13:12:45 -0400376 return animateColorAsState(
Max Alfonso-Ying781cd8a2022-11-14 19:04:31 +0000377 targetValue = targetValue,
Max Yingcc44d972021-10-11 13:12:45 -0400378 animationSpec = tween(ItemAnimationDurationMillis)
379 )
380 }
381
Connie Shi071c4832022-07-12 19:09:26 -0400382 /** Represents the color of the indicator used for selected items. */
383 internal val indicatorColor: Color
Max Yingcc44d972021-10-11 13:12:45 -0400384 get() = selectedIndicatorColor
Connie Shi071c4832022-07-12 19:09:26 -0400385
386 override fun equals(other: Any?): Boolean {
387 if (this === other) return true
388 if (other == null || other !is NavigationBarItemColors) return false
389
390 if (selectedIconColor != other.selectedIconColor) return false
391 if (unselectedIconColor != other.unselectedIconColor) return false
392 if (selectedTextColor != other.selectedTextColor) return false
393 if (unselectedTextColor != other.unselectedTextColor) return false
394 if (selectedIndicatorColor != other.selectedIndicatorColor) return false
Max Alfonso-Ying781cd8a2022-11-14 19:04:31 +0000395 if (disabledIconColor != other.disabledIconColor) return false
396 if (disabledTextColor != other.disabledTextColor) return false
Connie Shi071c4832022-07-12 19:09:26 -0400397
398 return true
399 }
400 override fun hashCode(): Int {
401 var result = selectedIconColor.hashCode()
402 result = 31 * result + unselectedIconColor.hashCode()
403 result = 31 * result + selectedTextColor.hashCode()
404 result = 31 * result + unselectedTextColor.hashCode()
405 result = 31 * result + selectedIndicatorColor.hashCode()
Max Alfonso-Ying781cd8a2022-11-14 19:04:31 +0000406 result = 31 * result + disabledIconColor.hashCode()
407 result = 31 * result + disabledTextColor.hashCode()
Connie Shi071c4832022-07-12 19:09:26 -0400408
409 return result
410 }
Max Yingcc44d972021-10-11 13:12:45 -0400411}
412
413/**
414 * Base layout for a [NavigationBarItem].
415 *
Max Ying51d3d9e2022-04-08 17:59:07 +0000416 * @param indicatorRipple indicator ripple for this item when it is selected
Max Yingcc44d972021-10-11 13:12:45 -0400417 * @param indicator indicator for this item when it is selected
418 * @param icon icon for this item
419 * @param label text label for this item
420 * @param alwaysShowLabel whether to always show the label for this item. If false, the label will
421 * only be shown when this item is selected.
422 * @param animationProgress progress of the animation, where 0 represents the unselected state of
423 * this item and 1 represents the selected state. This value controls other values such as indicator
424 * size, icon and label positions, etc.
425 */
426@Composable
427private fun NavigationBarItemBaselineLayout(
Max Ying51d3d9e2022-04-08 17:59:07 +0000428 indicatorRipple: @Composable () -> Unit,
Max Yingcc44d972021-10-11 13:12:45 -0400429 indicator: @Composable () -> Unit,
430 icon: @Composable () -> Unit,
431 label: @Composable (() -> Unit)?,
432 alwaysShowLabel: Boolean,
433 animationProgress: Float,
434) {
435 Layout({
Max Ying51d3d9e2022-04-08 17:59:07 +0000436 indicatorRipple()
Max Yingcc44d972021-10-11 13:12:45 -0400437 if (animationProgress > 0) {
438 indicator()
439 }
440
441 Box(Modifier.layoutId(IconLayoutIdTag)) { icon() }
442
443 if (label != null) {
444 Box(
Matvei Malkov848ce532022-08-04 16:56:35 +0100445 Modifier
446 .layoutId(LabelLayoutIdTag)
Max Yingcc44d972021-10-11 13:12:45 -0400447 .alpha(if (alwaysShowLabel) 1f else animationProgress)
Max Ying51d3d9e2022-04-08 17:59:07 +0000448 .padding(horizontal = NavigationBarItemHorizontalPadding / 2)
Max Yingcc44d972021-10-11 13:12:45 -0400449 ) { label() }
450 }
451 }) { measurables, constraints ->
452 val iconPlaceable =
453 measurables.first { it.layoutId == IconLayoutIdTag }.measure(constraints)
454
455 val totalIndicatorWidth = iconPlaceable.width + (IndicatorHorizontalPadding * 2).roundToPx()
456 val animatedIndicatorWidth = (totalIndicatorWidth * animationProgress).roundToInt()
457 val indicatorHeight = iconPlaceable.height + (IndicatorVerticalPadding * 2).roundToPx()
Max Ying51d3d9e2022-04-08 17:59:07 +0000458 val indicatorRipplePlaceable =
459 measurables
460 .first { it.layoutId == IndicatorRippleLayoutIdTag }
461 .measure(
462 Constraints.fixed(
463 width = totalIndicatorWidth,
464 height = indicatorHeight
465 )
466 )
Max Yingcc44d972021-10-11 13:12:45 -0400467 val indicatorPlaceable =
468 measurables
469 .firstOrNull { it.layoutId == IndicatorLayoutIdTag }
470 ?.measure(
471 Constraints.fixed(
472 width = animatedIndicatorWidth,
473 height = indicatorHeight
474 )
475 )
476
477 val labelPlaceable =
478 label?.let {
479 measurables
480 .first { it.layoutId == LabelLayoutIdTag }
481 .measure(
Max Ying2384bc82022-04-28 20:20:58 +0000482 // Measure with loose constraints for height as we don't want the label to
483 // take up more space than it needs
Max Yingcc44d972021-10-11 13:12:45 -0400484 constraints.copy(minHeight = 0)
485 )
486 }
487
488 if (label == null) {
Max Ying51d3d9e2022-04-08 17:59:07 +0000489 placeIcon(iconPlaceable, indicatorRipplePlaceable, indicatorPlaceable, constraints)
Max Yingcc44d972021-10-11 13:12:45 -0400490 } else {
491 placeLabelAndIcon(
492 labelPlaceable!!,
493 iconPlaceable,
Max Ying51d3d9e2022-04-08 17:59:07 +0000494 indicatorRipplePlaceable,
Max Yingcc44d972021-10-11 13:12:45 -0400495 indicatorPlaceable,
496 constraints,
497 alwaysShowLabel,
498 animationProgress
499 )
500 }
501 }
502}
503
504/**
Max Ying51d3d9e2022-04-08 17:59:07 +0000505 * Places the provided [Placeable]s in the center of the provided [constraints].
Max Yingcc44d972021-10-11 13:12:45 -0400506 */
507private fun MeasureScope.placeIcon(
508 iconPlaceable: Placeable,
Max Ying51d3d9e2022-04-08 17:59:07 +0000509 indicatorRipplePlaceable: Placeable,
Max Yingcc44d972021-10-11 13:12:45 -0400510 indicatorPlaceable: Placeable?,
511 constraints: Constraints
512): MeasureResult {
513 val width = constraints.maxWidth
514 val height = constraints.maxHeight
515
516 val iconX = (width - iconPlaceable.width) / 2
517 val iconY = (height - iconPlaceable.height) / 2
518
Max Ying51d3d9e2022-04-08 17:59:07 +0000519 val rippleX = (width - indicatorRipplePlaceable.width) / 2
520 val rippleY = (height - indicatorRipplePlaceable.height) / 2
521
Max Yingcc44d972021-10-11 13:12:45 -0400522 return layout(width, height) {
523 indicatorPlaceable?.let {
524 val indicatorX = (width - it.width) / 2
525 val indicatorY = (height - it.height) / 2
526 it.placeRelative(indicatorX, indicatorY)
527 }
528 iconPlaceable.placeRelative(iconX, iconY)
Max Ying51d3d9e2022-04-08 17:59:07 +0000529 indicatorRipplePlaceable.placeRelative(rippleX, rippleY)
Max Yingcc44d972021-10-11 13:12:45 -0400530 }
531}
532
533/**
Max Ying51d3d9e2022-04-08 17:59:07 +0000534 * Places the provided [Placeable]s in the correct position, depending on [alwaysShowLabel] and
535 * [animationProgress].
Max Yingcc44d972021-10-11 13:12:45 -0400536 *
537 * When [alwaysShowLabel] is true, the positions do not move. The [iconPlaceable] will be placed
538 * near the top of the item and the [labelPlaceable] will be placed near the bottom, according to
539 * the spec.
540 *
541 * When [animationProgress] is 1 (representing the selected state), the positions will be the same
542 * as above.
543 *
544 * Otherwise, when [animationProgress] is 0, [iconPlaceable] will be placed in the center, like in
545 * [placeIcon], and [labelPlaceable] will not be shown.
546 *
547 * When [animationProgress] is animating between these values, [iconPlaceable] and [labelPlaceable]
548 * will be placed at a corresponding interpolated position.
549 *
Max Ying51d3d9e2022-04-08 17:59:07 +0000550 * [indicatorRipplePlaceable] and [indicatorPlaceable] will always be placed in such a way that to
551 * share the same center as [iconPlaceable].
Max Yingcc44d972021-10-11 13:12:45 -0400552 *
553 * @param labelPlaceable text label placeable inside this item
554 * @param iconPlaceable icon placeable inside this item
Max Ying51d3d9e2022-04-08 17:59:07 +0000555 * @param indicatorRipplePlaceable indicator ripple placeable inside this item
Max Yingcc44d972021-10-11 13:12:45 -0400556 * @param indicatorPlaceable indicator placeable inside this item, if it exists
557 * @param constraints constraints of the item
558 * @param alwaysShowLabel whether to always show the label for this item. If true, icon and label
559 * positions will not change. If false, positions transition between 'centered icon with no label'
560 * and 'top aligned icon with label'.
561 * @param animationProgress progress of the animation, where 0 represents the unselected state of
562 * this item and 1 represents the selected state. Values between 0 and 1 interpolate positions of
563 * the icon and label.
564 */
565private fun MeasureScope.placeLabelAndIcon(
566 labelPlaceable: Placeable,
567 iconPlaceable: Placeable,
Max Ying51d3d9e2022-04-08 17:59:07 +0000568 indicatorRipplePlaceable: Placeable,
Max Yingcc44d972021-10-11 13:12:45 -0400569 indicatorPlaceable: Placeable?,
570 constraints: Constraints,
571 alwaysShowLabel: Boolean,
572 animationProgress: Float,
573): MeasureResult {
574 val height = constraints.maxHeight
575
Max Yingcc44d972021-10-11 13:12:45 -0400576 // Label should be `ItemVerticalPadding` from the bottom
Max Ying054270d2022-06-28 15:18:44 +0000577 val labelY = height - labelPlaceable.height - NavigationBarItemVerticalPadding.roundToPx()
Max Yingcc44d972021-10-11 13:12:45 -0400578
579 // Icon (when selected) should be `ItemVerticalPadding` from the top
580 val selectedIconY = NavigationBarItemVerticalPadding.roundToPx()
581 val unselectedIconY =
582 if (alwaysShowLabel) selectedIconY else (height - iconPlaceable.height) / 2
583
584 // How far the icon needs to move between unselected and selected states.
585 val iconDistance = unselectedIconY - selectedIconY
586
587 // The interpolated fraction of iconDistance that all placeables need to move based on
588 // animationProgress.
589 val offset = (iconDistance * (1 - animationProgress)).roundToInt()
590
591 val containerWidth = constraints.maxWidth
592
593 val labelX = (containerWidth - labelPlaceable.width) / 2
594 val iconX = (containerWidth - iconPlaceable.width) / 2
595
Max Ying51d3d9e2022-04-08 17:59:07 +0000596 val rippleX = (containerWidth - indicatorRipplePlaceable.width) / 2
597 val rippleY = selectedIconY - IndicatorVerticalPadding.roundToPx()
598
Max Yingcc44d972021-10-11 13:12:45 -0400599 return layout(containerWidth, height) {
600 indicatorPlaceable?.let {
601 val indicatorX = (containerWidth - it.width) / 2
602 val indicatorY = selectedIconY - IndicatorVerticalPadding.roundToPx()
603 it.placeRelative(indicatorX, indicatorY + offset)
604 }
605 if (alwaysShowLabel || animationProgress != 0f) {
606 labelPlaceable.placeRelative(labelX, labelY + offset)
607 }
608 iconPlaceable.placeRelative(iconX, selectedIconY + offset)
Max Ying51d3d9e2022-04-08 17:59:07 +0000609 indicatorRipplePlaceable.placeRelative(rippleX, rippleY + offset)
Max Yingcc44d972021-10-11 13:12:45 -0400610 }
611}
612
Max Ying51d3d9e2022-04-08 17:59:07 +0000613private const val IndicatorRippleLayoutIdTag: String = "indicatorRipple"
614
Max Yingcc44d972021-10-11 13:12:45 -0400615private const val IndicatorLayoutIdTag: String = "indicator"
616
617private const val IconLayoutIdTag: String = "icon"
618
619private const val LabelLayoutIdTag: String = "label"
620
Jose Alba Aguadofe2421c2021-11-02 17:37:45 +0100621private val NavigationBarHeight: Dp = NavigationBarTokens.ContainerHeight
Max Yingcc44d972021-10-11 13:12:45 -0400622
623private const val ItemAnimationDurationMillis: Int = 100
624
Max Ying51d3d9e2022-04-08 17:59:07 +0000625/*@VisibleForTesting*/
626internal val NavigationBarItemHorizontalPadding: Dp = 8.dp
Max Yingcc44d972021-10-11 13:12:45 -0400627
628/*@VisibleForTesting*/
629internal val NavigationBarItemVerticalPadding: Dp = 16.dp
630
Max Yingcc44d972021-10-11 13:12:45 -0400631private val IndicatorHorizontalPadding: Dp =
Jose Alba Aguadofe2421c2021-11-02 17:37:45 +0100632 (NavigationBarTokens.ActiveIndicatorWidth - NavigationBarTokens.IconSize) / 2
Max Yingcc44d972021-10-11 13:12:45 -0400633
634private val IndicatorVerticalPadding: Dp =
Max Ying51d3d9e2022-04-08 17:59:07 +0000635 (NavigationBarTokens.ActiveIndicatorHeight - NavigationBarTokens.IconSize) / 2
636
637private val IndicatorVerticalOffset: Dp = 12.dp