[go: nahoru, domu]

blob: 5fc7b6f611620c1cc3f3eae32c3f0b9d38c12883 [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
23import androidx.compose.foundation.interaction.Interaction
24import androidx.compose.foundation.interaction.MutableInteractionSource
25import androidx.compose.foundation.layout.Arrangement
26import androidx.compose.foundation.layout.Box
27import androidx.compose.foundation.layout.Row
28import androidx.compose.foundation.layout.RowScope
29import androidx.compose.foundation.layout.fillMaxWidth
30import androidx.compose.foundation.layout.height
31import androidx.compose.foundation.layout.padding
32import androidx.compose.foundation.selection.selectable
33import androidx.compose.foundation.selection.selectableGroup
34import androidx.compose.material.ripple.rememberRipple
Jose Alba Aguadofe2421c2021-11-02 17:37:45 +010035import androidx.compose.material3.tokens.NavigationBarTokens
Max Yingcc44d972021-10-11 13:12:45 -040036import androidx.compose.runtime.Composable
37import androidx.compose.runtime.CompositionLocalProvider
38import androidx.compose.runtime.Stable
39import androidx.compose.runtime.State
40import androidx.compose.runtime.getValue
41import androidx.compose.runtime.remember
42import androidx.compose.ui.Alignment
43import androidx.compose.ui.Modifier
44import androidx.compose.ui.draw.alpha
45import androidx.compose.ui.graphics.Color
Max Yingcc44d972021-10-11 13:12:45 -040046import androidx.compose.ui.layout.LastBaseline
47import androidx.compose.ui.layout.Layout
48import androidx.compose.ui.layout.MeasureResult
49import androidx.compose.ui.layout.MeasureScope
50import androidx.compose.ui.layout.Placeable
51import androidx.compose.ui.layout.layoutId
52import androidx.compose.ui.semantics.Role
53import androidx.compose.ui.unit.Constraints
54import androidx.compose.ui.unit.Dp
55import androidx.compose.ui.unit.dp
56import kotlin.math.roundToInt
57
58/**
Shalom Giblyf42290e2022-04-12 02:12:48 -070059 * <a href="https://m3.material.io/components/navigation-bar/overview" class="external" target="_blank">Material Design bottom navigation bar</a>.
60 *
61 * Navigation bars offer a persistent and convenient way to switch between primary destinations in
62 * an app.
63 *
Nick Rout2f69d9402021-10-19 16:25:18 +020064 * ![Navigation bar image](https://developer.android.com/images/reference/androidx/compose/material3/navigation-bar.png)
65 *
Max Yingcc44d972021-10-11 13:12:45 -040066 * [NavigationBar] should contain three to five [NavigationBarItem]s, each representing a singular
67 * destination.
68 *
69 * A simple example looks like:
70 * @sample androidx.compose.material3.samples.NavigationBarSample
71 *
72 * See [NavigationBarItem] for configuration specific to each item, and not the overall
73 * [NavigationBar] component.
74 *
Max Ying2384bc82022-04-28 20:20:58 +000075 * @param modifier the [Modifier] to be applied to this navigation bar
76 * @param containerColor the color used for the background of this navigation bar. Use
77 * [Color.Transparent] to have no color.
78 * @param contentColor the preferred color for content inside this navigation bar. Defaults to
79 * either the matching content color for [containerColor], or to the current [LocalContentColor] if
80 * [containerColor] is not a color from the theme.
81 * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
82 * overlay is applied on top of the container. A higher tonal elevation value will result in a
83 * darker color in light theme and lighter color in dark theme. See also: [Surface].
84 * @param content the content of this navigation bar, typically 3-5 [NavigationBarItem]s
Max Yingcc44d972021-10-11 13:12:45 -040085 */
86@Composable
87fun NavigationBar(
88 modifier: Modifier = Modifier,
Mariano15a489b2022-01-19 13:08:12 -050089 containerColor: Color = NavigationBarTokens.ContainerColor.toColor(),
Max Yingcc44d972021-10-11 13:12:45 -040090 contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor),
Jose Alba Aguadofe2421c2021-11-02 17:37:45 +010091 tonalElevation: Dp = NavigationBarTokens.ContainerElevation,
Max Yingcc44d972021-10-11 13:12:45 -040092 content: @Composable RowScope.() -> Unit
93) {
94 Surface(
95 color = containerColor,
96 contentColor = contentColor,
97 tonalElevation = tonalElevation,
98 modifier = modifier
99 ) {
100 Row(
101 modifier = Modifier.fillMaxWidth().height(NavigationBarHeight).selectableGroup(),
102 horizontalArrangement = Arrangement.SpaceBetween,
103 content = content
104 )
105 }
106}
107
108/**
109 * Material Design navigation bar item.
110 *
Shalom Giblyf42290e2022-04-12 02:12:48 -0700111 * Navigation bars offer a persistent and convenient way to switch between primary destinations in
112 * an app.
113 *
Max Yingcc44d972021-10-11 13:12:45 -0400114 * The recommended configuration for a [NavigationBarItem] depends on how many items there are
115 * inside a [NavigationBar]:
116 *
117 * - Three destinations: Display icons and text labels for all destinations.
118 * - Four destinations: Active destinations display an icon and text label. Inactive destinations
119 * display icons, and text labels are recommended.
120 * - Five destinations: Active destinations display an icon and text label. Inactive destinations
121 * use icons, and use text labels if space permits.
122 *
123 * A [NavigationBarItem] always shows text labels (if it exists) when selected. Showing text
124 * labels if not selected is controlled by [alwaysShowLabel].
125 *
126 * @param selected whether this item is selected
Max Ying2384bc82022-04-28 20:20:58 +0000127 * @param onClick called when this item is clicked
128 * @param icon icon for this item, typically an [Icon]
129 * @param modifier the [Modifier] to be applied to this item
130 * @param enabled controls the enabled state of this item. When `false`, this component will not
131 * respond to user input, and it will appear visually disabled and disabled to accessibility
132 * services.
Max Yingcc44d972021-10-11 13:12:45 -0400133 * @param label optional text label for this item
Max Ying2384bc82022-04-28 20:20:58 +0000134 * @param alwaysShowLabel whether to always show the label for this item. If `false`, the label will
Max Yingcc44d972021-10-11 13:12:45 -0400135 * only be shown when this item is selected.
136 * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
Max Ying2384bc82022-04-28 20:20:58 +0000137 * for this item. You can create and pass in your own `remember`ed instance to observe
138 * [Interaction]s and customize the appearance / behavior of this item in different states.
139 * @param colors [NavigationBarItemColors] that will be used to resolve the colors used for this
140 * item in different states. See [NavigationBarItemDefaults.colors].
Max Yingcc44d972021-10-11 13:12:45 -0400141 */
142@Composable
143fun RowScope.NavigationBarItem(
144 selected: Boolean,
145 onClick: () -> Unit,
146 icon: @Composable () -> Unit,
147 modifier: Modifier = Modifier,
148 enabled: Boolean = true,
149 label: @Composable (() -> Unit)? = null,
150 alwaysShowLabel: Boolean = true,
151 interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
152 colors: NavigationBarItemColors = NavigationBarItemDefaults.colors()
153) {
154 val styledIcon = @Composable {
155 val iconColor by colors.iconColor(selected = selected)
156 CompositionLocalProvider(LocalContentColor provides iconColor, content = icon)
157 }
158
159 val styledLabel: @Composable (() -> Unit)? = label?.let {
160 @Composable {
Jose Alba Aguadofe2421c2021-11-02 17:37:45 +0100161 val style = MaterialTheme.typography.fromToken(NavigationBarTokens.LabelTextFont)
Max Yingcc44d972021-10-11 13:12:45 -0400162 val textColor by colors.textColor(selected = selected)
163 CompositionLocalProvider(LocalContentColor provides textColor) {
164 ProvideTextStyle(style, content = label)
165 }
166 }
167 }
168
169 Box(
170 modifier
171 .selectable(
172 selected = selected,
173 onClick = onClick,
174 enabled = enabled,
175 role = Role.Tab,
176 interactionSource = interactionSource,
177 indication = rememberRipple(),
178 )
179 .weight(1f),
180 contentAlignment = Alignment.Center
181 ) {
182 val animationProgress: Float by animateFloatAsState(
183 targetValue = if (selected) 1f else 0f,
184 animationSpec = tween(ItemAnimationDurationMillis)
185 )
186
187 val indicator = @Composable {
188 Box(
189 Modifier.layoutId(IndicatorLayoutIdTag)
190 .background(
191 color = colors.indicatorColor.copy(alpha = animationProgress),
Jose Alba Aguado32056032022-03-22 12:50:19 +0100192 shape = NavigationBarTokens.ActiveIndicatorShape.toShape(),
Max Yingcc44d972021-10-11 13:12:45 -0400193 )
194 )
195 }
196
197 NavigationBarItemBaselineLayout(
198 indicator = indicator,
199 icon = styledIcon,
200 label = styledLabel,
201 alwaysShowLabel = alwaysShowLabel,
202 animationProgress = animationProgress
203 )
204 }
205}
206
207/** Defaults used in [NavigationBarItem]. */
208object NavigationBarItemDefaults {
209 /**
210 * Creates a [NavigationBarItemColors] with the provided colors according to the Material
211 * specification.
212 *
213 * @param selectedIconColor the color to use for the icon when the item is selected.
214 * @param unselectedIconColor the color to use for the icon when the item is unselected.
215 * @param selectedTextColor the color to use for the text label when the item is selected.
216 * @param unselectedTextColor the color to use for the text label when the item is unselected.
217 * @param indicatorColor the color to use for the indicator when the item is selected.
218 * @return the resulting [NavigationBarItemColors] used for [NavigationBarItem]
219 */
220 @Composable
221 fun colors(
Mariano15a489b2022-01-19 13:08:12 -0500222 selectedIconColor: Color = NavigationBarTokens.ActiveIconColor.toColor(),
223 unselectedIconColor: Color = NavigationBarTokens.InactiveIconColor.toColor(),
224 selectedTextColor: Color = NavigationBarTokens.ActiveLabelTextColor.toColor(),
225 unselectedTextColor: Color = NavigationBarTokens.InactiveLabelTextColor.toColor(),
226 indicatorColor: Color = NavigationBarTokens.ActiveIndicatorColor.toColor(),
Max Yingcc44d972021-10-11 13:12:45 -0400227 ): NavigationBarItemColors = remember(
228 selectedIconColor,
229 unselectedIconColor,
230 selectedTextColor,
231 unselectedTextColor,
232 indicatorColor
233 ) {
234 DefaultNavigationBarItemColors(
235 selectedIconColor = selectedIconColor,
236 unselectedIconColor = unselectedIconColor,
237 selectedTextColor = selectedTextColor,
238 unselectedTextColor = unselectedTextColor,
239 selectedIndicatorColor = indicatorColor,
240 )
241 }
242}
243
244/** Represents the colors of the various elements of a navigation item. */
245@Stable
246interface NavigationBarItemColors {
247 /**
248 * Represents the icon color for this item, depending on whether it is [selected].
249 *
250 * @param selected whether the item is selected
251 */
252 @Composable
253 fun iconColor(selected: Boolean): State<Color>
254
255 /**
256 * Represents the text color for this item, depending on whether it is [selected].
257 *
258 * @param selected whether the item is selected
259 */
260 @Composable
261 fun textColor(selected: Boolean): State<Color>
262
263 /** Represents the color of the indicator used for selected items. */
264 val indicatorColor: Color
265 @Composable get
266}
267
268@Stable
269private class DefaultNavigationBarItemColors(
270 private val selectedIconColor: Color,
271 private val unselectedIconColor: Color,
272 private val selectedTextColor: Color,
273 private val unselectedTextColor: Color,
274 private val selectedIndicatorColor: Color,
275) : NavigationBarItemColors {
276 @Composable
277 override fun iconColor(selected: Boolean): State<Color> {
278 return animateColorAsState(
279 targetValue = if (selected) selectedIconColor else unselectedIconColor,
280 animationSpec = tween(ItemAnimationDurationMillis)
281 )
282 }
283
284 @Composable
285 override fun textColor(selected: Boolean): State<Color> {
286 return animateColorAsState(
287 targetValue = if (selected) selectedTextColor else unselectedTextColor,
288 animationSpec = tween(ItemAnimationDurationMillis)
289 )
290 }
291
292 override val indicatorColor: Color
293 @Composable
294 get() = selectedIndicatorColor
295}
296
297/**
298 * Base layout for a [NavigationBarItem].
299 *
300 * @param indicator indicator for this item when it is selected
301 * @param icon icon for this item
302 * @param label text label for this item
303 * @param alwaysShowLabel whether to always show the label for this item. If false, the label will
304 * only be shown when this item is selected.
305 * @param animationProgress progress of the animation, where 0 represents the unselected state of
306 * this item and 1 represents the selected state. This value controls other values such as indicator
307 * size, icon and label positions, etc.
308 */
309@Composable
310private fun NavigationBarItemBaselineLayout(
311 indicator: @Composable () -> Unit,
312 icon: @Composable () -> Unit,
313 label: @Composable (() -> Unit)?,
314 alwaysShowLabel: Boolean,
315 animationProgress: Float,
316) {
317 Layout({
318 if (animationProgress > 0) {
319 indicator()
320 }
321
322 Box(Modifier.layoutId(IconLayoutIdTag)) { icon() }
323
324 if (label != null) {
325 Box(
326 Modifier.layoutId(LabelLayoutIdTag)
327 .alpha(if (alwaysShowLabel) 1f else animationProgress)
328 .padding(horizontal = NavigationBarItemHorizontalPadding)
329 ) { label() }
330 }
331 }) { measurables, constraints ->
332 val iconPlaceable =
333 measurables.first { it.layoutId == IconLayoutIdTag }.measure(constraints)
334
335 val totalIndicatorWidth = iconPlaceable.width + (IndicatorHorizontalPadding * 2).roundToPx()
336 val animatedIndicatorWidth = (totalIndicatorWidth * animationProgress).roundToInt()
337 val indicatorHeight = iconPlaceable.height + (IndicatorVerticalPadding * 2).roundToPx()
338 val indicatorPlaceable =
339 measurables
340 .firstOrNull { it.layoutId == IndicatorLayoutIdTag }
341 ?.measure(
342 Constraints.fixed(
343 width = animatedIndicatorWidth,
344 height = indicatorHeight
345 )
346 )
347
348 val labelPlaceable =
349 label?.let {
350 measurables
351 .first { it.layoutId == LabelLayoutIdTag }
352 .measure(
Max Ying2384bc82022-04-28 20:20:58 +0000353 // Measure with loose constraints for height as we don't want the label to
354 // take up more space than it needs
Max Yingcc44d972021-10-11 13:12:45 -0400355 constraints.copy(minHeight = 0)
356 )
357 }
358
359 if (label == null) {
360 placeIcon(iconPlaceable, indicatorPlaceable, constraints)
361 } else {
362 placeLabelAndIcon(
363 labelPlaceable!!,
364 iconPlaceable,
365 indicatorPlaceable,
366 constraints,
367 alwaysShowLabel,
368 animationProgress
369 )
370 }
371 }
372}
373
374/**
375 * Places the provided [iconPlaceable], and possibly [indicatorPlaceable] if it exists, in the
376 * center of the provided [constraints].
377 */
378private fun MeasureScope.placeIcon(
379 iconPlaceable: Placeable,
380 indicatorPlaceable: Placeable?,
381 constraints: Constraints
382): MeasureResult {
383 val width = constraints.maxWidth
384 val height = constraints.maxHeight
385
386 val iconX = (width - iconPlaceable.width) / 2
387 val iconY = (height - iconPlaceable.height) / 2
388
389 return layout(width, height) {
390 indicatorPlaceable?.let {
391 val indicatorX = (width - it.width) / 2
392 val indicatorY = (height - it.height) / 2
393 it.placeRelative(indicatorX, indicatorY)
394 }
395 iconPlaceable.placeRelative(iconX, iconY)
396 }
397}
398
399/**
400 * Places the provided [labelPlaceable], [iconPlaceable], and [indicatorPlaceable] in the correct
401 * position, depending on [alwaysShowLabel] and [animationProgress].
402 *
403 * When [alwaysShowLabel] is true, the positions do not move. The [iconPlaceable] will be placed
404 * near the top of the item and the [labelPlaceable] will be placed near the bottom, according to
405 * the spec.
406 *
407 * When [animationProgress] is 1 (representing the selected state), the positions will be the same
408 * as above.
409 *
410 * Otherwise, when [animationProgress] is 0, [iconPlaceable] will be placed in the center, like in
411 * [placeIcon], and [labelPlaceable] will not be shown.
412 *
413 * When [animationProgress] is animating between these values, [iconPlaceable] and [labelPlaceable]
414 * will be placed at a corresponding interpolated position.
415 *
416 * [indicatorPlaceable] will always be placed in such a way that it shares the same center as
417 * [iconPlaceable].
418 *
419 * @param labelPlaceable text label placeable inside this item
420 * @param iconPlaceable icon placeable inside this item
421 * @param indicatorPlaceable indicator placeable inside this item, if it exists
422 * @param constraints constraints of the item
423 * @param alwaysShowLabel whether to always show the label for this item. If true, icon and label
424 * positions will not change. If false, positions transition between 'centered icon with no label'
425 * and 'top aligned icon with label'.
426 * @param animationProgress progress of the animation, where 0 represents the unselected state of
427 * this item and 1 represents the selected state. Values between 0 and 1 interpolate positions of
428 * the icon and label.
429 */
430private fun MeasureScope.placeLabelAndIcon(
431 labelPlaceable: Placeable,
432 iconPlaceable: Placeable,
433 indicatorPlaceable: Placeable?,
434 constraints: Constraints,
435 alwaysShowLabel: Boolean,
436 animationProgress: Float,
437): MeasureResult {
438 val height = constraints.maxHeight
439
440 val baseline = labelPlaceable[LastBaseline]
441 // Label should be `ItemVerticalPadding` from the bottom
442 val labelY = height - baseline - NavigationBarItemVerticalPadding.roundToPx()
443
444 // Icon (when selected) should be `ItemVerticalPadding` from the top
445 val selectedIconY = NavigationBarItemVerticalPadding.roundToPx()
446 val unselectedIconY =
447 if (alwaysShowLabel) selectedIconY else (height - iconPlaceable.height) / 2
448
449 // How far the icon needs to move between unselected and selected states.
450 val iconDistance = unselectedIconY - selectedIconY
451
452 // The interpolated fraction of iconDistance that all placeables need to move based on
453 // animationProgress.
454 val offset = (iconDistance * (1 - animationProgress)).roundToInt()
455
456 val containerWidth = constraints.maxWidth
457
458 val labelX = (containerWidth - labelPlaceable.width) / 2
459 val iconX = (containerWidth - iconPlaceable.width) / 2
460
461 return layout(containerWidth, height) {
462 indicatorPlaceable?.let {
463 val indicatorX = (containerWidth - it.width) / 2
464 val indicatorY = selectedIconY - IndicatorVerticalPadding.roundToPx()
465 it.placeRelative(indicatorX, indicatorY + offset)
466 }
467 if (alwaysShowLabel || animationProgress != 0f) {
468 labelPlaceable.placeRelative(labelX, labelY + offset)
469 }
470 iconPlaceable.placeRelative(iconX, selectedIconY + offset)
471 }
472}
473
474private const val IndicatorLayoutIdTag: String = "indicator"
475
476private const val IconLayoutIdTag: String = "icon"
477
478private const val LabelLayoutIdTag: String = "label"
479
Jose Alba Aguadofe2421c2021-11-02 17:37:45 +0100480private val NavigationBarHeight: Dp = NavigationBarTokens.ContainerHeight
Max Yingcc44d972021-10-11 13:12:45 -0400481
482private const val ItemAnimationDurationMillis: Int = 100
483
484private val NavigationBarItemHorizontalPadding: Dp = 4.dp
485
486/*@VisibleForTesting*/
487internal val NavigationBarItemVerticalPadding: Dp = 16.dp
488
Max Yingcc44d972021-10-11 13:12:45 -0400489private val IndicatorHorizontalPadding: Dp =
Jose Alba Aguadofe2421c2021-11-02 17:37:45 +0100490 (NavigationBarTokens.ActiveIndicatorWidth - NavigationBarTokens.IconSize) / 2
Max Yingcc44d972021-10-11 13:12:45 -0400491
492private val IndicatorVerticalPadding: Dp =
Jose Alba Aguadofe2421c2021-11-02 17:37:45 +0100493 (NavigationBarTokens.ActiveIndicatorHeight - NavigationBarTokens.IconSize) / 2