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.core.AnimationSpec
20 import androidx.compose.animation.core.FiniteAnimationSpec
21 import androidx.compose.animation.core.TweenSpec
22 import androidx.compose.animation.core.animate
23 import androidx.compose.animation.core.snap
24 import androidx.compose.foundation.Canvas
25 import androidx.compose.foundation.gestures.Orientation
26 import androidx.compose.foundation.gestures.detectTapGestures
27 import androidx.compose.foundation.interaction.Interaction
28 import androidx.compose.foundation.interaction.MutableInteractionSource
29 import androidx.compose.foundation.layout.Box
30 import androidx.compose.foundation.layout.Column
31 import androidx.compose.foundation.layout.ColumnScope
32 import androidx.compose.foundation.layout.PaddingValues
33 import androidx.compose.foundation.layout.Row
34 import androidx.compose.foundation.layout.Spacer
35 import androidx.compose.foundation.layout.WindowInsets
36 import androidx.compose.foundation.layout.WindowInsetsSides
37 import androidx.compose.foundation.layout.fillMaxHeight
38 import androidx.compose.foundation.layout.fillMaxSize
39 import androidx.compose.foundation.layout.fillMaxWidth
40 import androidx.compose.foundation.layout.heightIn
41 import androidx.compose.foundation.layout.offset
42 import androidx.compose.foundation.layout.only
43 import androidx.compose.foundation.layout.padding
44 import androidx.compose.foundation.layout.sizeIn
45 import androidx.compose.foundation.layout.width
46 import androidx.compose.foundation.layout.windowInsetsPadding
47 import androidx.compose.material3.internal.AnchoredDraggableState
48 import androidx.compose.material3.internal.BackEventCompat
49 import androidx.compose.material3.internal.DraggableAnchors
50 import androidx.compose.material3.internal.FloatProducer
51 import androidx.compose.material3.internal.PredictiveBack
52 import androidx.compose.material3.internal.PredictiveBackHandler
53 import androidx.compose.material3.internal.Strings
54 import androidx.compose.material3.internal.anchoredDraggable
55 import androidx.compose.material3.internal.getString
56 import androidx.compose.material3.internal.snapTo
57 import androidx.compose.material3.internal.systemBarsForVisualComponents
58 import androidx.compose.material3.tokens.ElevationTokens
59 import androidx.compose.material3.tokens.MotionSchemeKeyTokens
60 import androidx.compose.material3.tokens.NavigationDrawerTokens
61 import androidx.compose.material3.tokens.ScrimTokens
62 import androidx.compose.runtime.Composable
63 import androidx.compose.runtime.CompositionLocalProvider
64 import androidx.compose.runtime.LaunchedEffect
65 import androidx.compose.runtime.SideEffect
66 import androidx.compose.runtime.Stable
67 import androidx.compose.runtime.State
68 import androidx.compose.runtime.getValue
69 import androidx.compose.runtime.mutableFloatStateOf
70 import androidx.compose.runtime.mutableStateOf
71 import androidx.compose.runtime.remember
72 import androidx.compose.runtime.rememberCoroutineScope
73 import androidx.compose.runtime.rememberUpdatedState
74 import androidx.compose.runtime.saveable.Saver
75 import androidx.compose.runtime.saveable.rememberSaveable
76 import androidx.compose.runtime.setValue
77 import androidx.compose.ui.Alignment
78 import androidx.compose.ui.Modifier
79 import androidx.compose.ui.graphics.Color
80 import androidx.compose.ui.graphics.GraphicsLayerScope
81 import androidx.compose.ui.graphics.RectangleShape
82 import androidx.compose.ui.graphics.Shape
83 import androidx.compose.ui.graphics.TransformOrigin
84 import androidx.compose.ui.graphics.graphicsLayer
85 import androidx.compose.ui.input.pointer.pointerInput
86 import androidx.compose.ui.layout.Layout
87 import androidx.compose.ui.platform.LocalDensity
88 import androidx.compose.ui.platform.LocalLayoutDirection
89 import androidx.compose.ui.semantics.Role
90 import androidx.compose.ui.semantics.contentDescription
91 import androidx.compose.ui.semantics.dismiss
92 import androidx.compose.ui.semantics.onClick
93 import androidx.compose.ui.semantics.paneTitle
94 import androidx.compose.ui.semantics.role
95 import androidx.compose.ui.semantics.semantics
96 import androidx.compose.ui.unit.Density
97 import androidx.compose.ui.unit.Dp
98 import androidx.compose.ui.unit.IntOffset
99 import androidx.compose.ui.unit.LayoutDirection
100 import androidx.compose.ui.unit.dp
101 import androidx.compose.ui.util.fastForEach
102 import androidx.compose.ui.util.fastMap
103 import androidx.compose.ui.util.fastMaxOfOrNull
104 import androidx.compose.ui.util.lerp
105 import kotlin.math.roundToInt
106 import kotlinx.coroutines.CancellationException
107 import kotlinx.coroutines.launch
108
109 /** Possible values of [DrawerState]. */
110 enum class DrawerValue {
111 /** The state of the drawer when it is closed. */
112 Closed,
113
114 /** The state of the drawer when it is open. */
115 Open
116 }
117
118 /**
119 * State of the [ModalNavigationDrawer] and [DismissibleNavigationDrawer] composable.
120 *
121 * @param initialValue The initial value of the state.
122 * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
123 */
124 @Suppress("NotCloseable")
125 @Stable
126 class DrawerState(
127 initialValue: DrawerValue,
<lambda>null128 confirmStateChange: (DrawerValue) -> Boolean = { true }
129 ) {
130
131 internal var anchoredDraggableMotionSpec: FiniteAnimationSpec<Float> =
132 AnchoredDraggableDefaultAnimationSpec
133
134 internal val anchoredDraggableState =
135 AnchoredDraggableState(
136 initialValue = initialValue,
<lambda>null137 animationSpec = { anchoredDraggableMotionSpec },
138 confirmValueChange = confirmStateChange,
distancenull139 positionalThreshold = { distance -> distance * DrawerPositionalThreshold },
<lambda>null140 velocityThreshold = { with(requireDensity()) { DrawerVelocityThreshold.toPx() } }
141 )
142
143 /** Whether the drawer is open. */
144 val isOpen: Boolean
145 get() = currentValue == DrawerValue.Open
146
147 /** Whether the drawer is closed. */
148 val isClosed: Boolean
149 get() = currentValue == DrawerValue.Closed
150
151 /**
152 * The current value of the state.
153 *
154 * If no swipe or animation is in progress, this corresponds to the start the drawer currently
155 * in. If a swipe or an animation is in progress, this corresponds the state drawer was in
156 * before the swipe or animation started.
157 */
158 val currentValue: DrawerValue
159 get() {
160 return anchoredDraggableState.currentValue
161 }
162
163 /** Whether the state is currently animating. */
164 val isAnimationRunning: Boolean
165 get() {
166 return anchoredDraggableState.isAnimationRunning
167 }
168
169 /**
170 * Open the drawer with animation and suspend until it if fully opened or animation has been
171 * cancelled. This method will throw [CancellationException] if the animation is interrupted
172 *
173 * @return the reason the open animation ended
174 */
opennull175 suspend fun open() =
176 animateTo(targetValue = DrawerValue.Open, animationSpec = openDrawerMotionSpec)
177
178 /**
179 * Close the drawer with animation and suspend until it if fully closed or animation has been
180 * cancelled. This method will throw [CancellationException] if the animation is interrupted
181 *
182 * @return the reason the close animation ended
183 */
184 suspend fun close() =
185 animateTo(targetValue = DrawerValue.Closed, animationSpec = closeDrawerMotionSpec)
186
187 /**
188 * Set the state of the drawer with specific animation
189 *
190 * @param targetValue The new value to animate to.
191 * @param anim The animation that will be used to animate to the new value.
192 */
193 @Deprecated(
194 message =
195 "This method has been replaced by the open and close methods. The animation " +
196 "spec is now an implementation detail of ModalDrawer.",
197 )
198 suspend fun animateTo(targetValue: DrawerValue, anim: AnimationSpec<Float>) {
199 animateTo(targetValue = targetValue, animationSpec = anim)
200 }
201
202 /**
203 * Set the state without any animation and suspend until it's set
204 *
205 * @param targetValue The new target value
206 */
snapTonull207 suspend fun snapTo(targetValue: DrawerValue) {
208 anchoredDraggableState.snapTo(targetValue)
209 }
210
211 /**
212 * The target value of the drawer state.
213 *
214 * If a swipe is in progress, this is the value that the Drawer would animate to if the swipe
215 * finishes. If an animation is running, this is the target value of that animation. Finally, if
216 * no swipe or animation is in progress, this is the same as the [currentValue].
217 */
218 val targetValue: DrawerValue
219 get() = anchoredDraggableState.targetValue
220
221 /**
222 * The current position (in pixels) of the drawer sheet, or Float.NaN before the offset is
223 * initialized.
224 *
225 * @see [AnchoredDraggableState.offset] for more information.
226 */
227 @Deprecated(
228 message =
229 "Please access the offset through currentOffset, which returns the value " +
230 "directly instead of wrapping it in a state object.",
231 replaceWith = ReplaceWith("currentOffset")
232 )
233 val offset: State<Float> =
234 object : State<Float> {
235 override val value: Float
236 get() = anchoredDraggableState.offset
237 }
238
239 /**
240 * The current position (in pixels) of the drawer sheet, or Float.NaN before the offset is
241 * initialized.
242 *
243 * @see [AnchoredDraggableState.offset] for more information.
244 */
245 val currentOffset: Float
246 get() = anchoredDraggableState.offset
247
248 internal var density: Density? by mutableStateOf(null)
249
250 internal var openDrawerMotionSpec: FiniteAnimationSpec<Float> = snap()
251
252 internal var closeDrawerMotionSpec: FiniteAnimationSpec<Float> = snap()
253
requireDensitynull254 private fun requireDensity() =
255 requireNotNull(density) {
256 "The density on DrawerState ($this) was not set. Did you use DrawerState" +
257 " with the ModalNavigationDrawer or DismissibleNavigationDrawer composables?"
258 }
259
requireOffsetnull260 internal fun requireOffset(): Float = anchoredDraggableState.requireOffset()
261
262 private suspend fun animateTo(
263 targetValue: DrawerValue,
264 animationSpec: AnimationSpec<Float>,
265 velocity: Float = anchoredDraggableState.lastVelocity
266 ) {
267 anchoredDraggableState.anchoredDrag(targetValue = targetValue) { anchors, latestTarget ->
268 val targetOffset = anchors.positionOf(latestTarget)
269 if (!targetOffset.isNaN()) {
270 var prev = if (currentOffset.isNaN()) 0f else currentOffset
271 animate(prev, targetOffset, velocity, animationSpec) { value, velocity ->
272 // Our onDrag coerces the value within the bounds, but an animation may
273 // overshoot, for example a spring animation or an overshooting interpolator
274 // We respect the user's intention and allow the overshoot, but still use
275 // DraggableState's drag for its mutex.
276 dragTo(value, velocity)
277 prev = value
278 }
279 }
280 }
281 }
282
283 companion object {
284 /** The default [Saver] implementation for [DrawerState]. */
Savernull285 fun Saver(confirmStateChange: (DrawerValue) -> Boolean) =
286 Saver<DrawerState, DrawerValue>(
287 save = { it.currentValue },
<lambda>null288 restore = { DrawerState(it, confirmStateChange) }
289 )
290 }
291 }
292
293 /**
294 * Create and [remember] a [DrawerState].
295 *
296 * @param initialValue The initial value of the state.
297 * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
298 */
299 @Composable
rememberDrawerStatenull300 fun rememberDrawerState(
301 initialValue: DrawerValue,
302 confirmStateChange: (DrawerValue) -> Boolean = { true }
303 ): DrawerState {
<lambda>null304 return rememberSaveable(saver = DrawerState.Saver(confirmStateChange)) {
305 DrawerState(initialValue, confirmStateChange)
306 }
307 }
308
309 /**
310 * [Material Design navigation drawer](https://m3.material.io/components/navigation-drawer/overview)
311 *
312 * Navigation drawers provide ergonomic access to destinations in an app.
313 *
314 * Modal navigation drawers block interaction with the rest of an app’s content with a scrim. They
315 * are elevated above most of the app’s UI and don’t affect the screen’s layout grid.
316 *
317 * 
319 *
320 * @sample androidx.compose.material3.samples.ModalNavigationDrawerSample
321 * @param drawerContent content inside this drawer
322 * @param modifier the [Modifier] to be applied to this drawer
323 * @param drawerState state of the drawer
324 * @param gesturesEnabled whether or not the drawer can be interacted by gestures
325 * @param scrimColor color of the scrim that obscures content when the drawer is open
326 * @param content content of the rest of the UI
327 */
328 @Composable
ModalNavigationDrawernull329 fun ModalNavigationDrawer(
330 drawerContent: @Composable () -> Unit,
331 modifier: Modifier = Modifier,
332 drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
333 gesturesEnabled: Boolean = true,
334 scrimColor: Color = DrawerDefaults.scrimColor,
335 content: @Composable () -> Unit
336 ) {
337 val scope = rememberCoroutineScope()
338 val navigationMenu = getString(Strings.NavigationMenu)
339 val density = LocalDensity.current
340 var anchorsInitialized by remember { mutableStateOf(false) }
341 var minValue by remember(density) { mutableFloatStateOf(0f) }
342 val maxValue = 0f
343
344 // TODO Load the motionScheme tokens from the component tokens file
345 val anchoredDraggableMotion: FiniteAnimationSpec<Float> =
346 MotionSchemeKeyTokens.DefaultSpatial.value()
347 val openMotion: FiniteAnimationSpec<Float> = MotionSchemeKeyTokens.DefaultSpatial.value()
348 val closeMotion: FiniteAnimationSpec<Float> = MotionSchemeKeyTokens.FastEffects.value()
349
350 SideEffect {
351 drawerState.density = density
352 drawerState.openDrawerMotionSpec = openMotion
353 drawerState.closeDrawerMotionSpec = closeMotion
354 drawerState.anchoredDraggableMotionSpec = anchoredDraggableMotion
355 }
356
357 val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
358 Box(
359 modifier
360 .fillMaxSize()
361 .anchoredDraggable(
362 state = drawerState.anchoredDraggableState,
363 orientation = Orientation.Horizontal,
364 enabled = gesturesEnabled,
365 reverseDirection = isRtl
366 )
367 ) {
368 Box { content() }
369 Scrim(
370 open = drawerState.isOpen,
371 onClose = {
372 if (
373 gesturesEnabled &&
374 drawerState.anchoredDraggableState.confirmValueChange(DrawerValue.Closed)
375 ) {
376 scope.launch { drawerState.close() }
377 }
378 },
379 fraction = { calculateFraction(minValue, maxValue, drawerState.requireOffset()) },
380 color = scrimColor
381 )
382 Layout(
383 content = drawerContent,
384 modifier =
385 Modifier.offset {
386 drawerState.currentOffset.let { offset ->
387 val offsetX =
388 when {
389 !offset.isNaN() -> offset.roundToInt()
390 // If offset is NaN, set offset based on open/closed state
391 drawerState.isOpen -> 0
392 else -> -DrawerDefaults.MaximumDrawerWidth.roundToPx()
393 }
394 IntOffset(offsetX, 0)
395 }
396 }
397 .semantics {
398 paneTitle = navigationMenu
399 if (drawerState.isOpen) {
400 dismiss {
401 if (
402 drawerState.anchoredDraggableState.confirmValueChange(
403 DrawerValue.Closed
404 )
405 ) {
406 scope.launch { drawerState.close() }
407 }
408 true
409 }
410 }
411 },
412 ) { measurables, constraints ->
413 val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
414 val placeables = measurables.fastMap { it.measure(looseConstraints) }
415 val width = placeables.fastMaxOfOrNull { it.width } ?: 0
416 val height = placeables.fastMaxOfOrNull { it.height } ?: 0
417
418 layout(width, height) {
419 val currentClosedAnchor =
420 drawerState.anchoredDraggableState.anchors.positionOf(DrawerValue.Closed)
421 val calculatedClosedAnchor = -width.toFloat()
422
423 if (!anchorsInitialized || currentClosedAnchor != calculatedClosedAnchor) {
424 if (!anchorsInitialized) {
425 anchorsInitialized = true
426 }
427 minValue = calculatedClosedAnchor
428 drawerState.anchoredDraggableState.updateAnchors(
429 DraggableAnchors {
430 DrawerValue.Closed at minValue
431 DrawerValue.Open at maxValue
432 }
433 )
434 }
435 placeables.fastForEach { it.placeRelative(0, 0) }
436 }
437 }
438 }
439 }
440
441 /**
442 * [Material Design navigation drawer](https://m3.material.io/components/navigation-drawer/overview)
443 *
444 * Navigation drawers provide ergonomic access to destinations in an app. They’re often next to app
445 * content and affect the screen’s layout grid.
446 *
447 * 
449 *
450 * Dismissible standard drawers can be used for layouts that prioritize content (such as a photo
451 * gallery) or for apps where users are unlikely to switch destinations often. They should use a
452 * visible navigation menu icon to open and close the drawer.
453 *
454 * @sample androidx.compose.material3.samples.DismissibleNavigationDrawerSample
455 * @param drawerContent content inside this drawer
456 * @param modifier the [Modifier] to be applied to this drawer
457 * @param drawerState state of the drawer
458 * @param gesturesEnabled whether or not the drawer can be interacted by gestures
459 * @param content content of the rest of the UI
460 */
461 @Composable
DismissibleNavigationDrawernull462 fun DismissibleNavigationDrawer(
463 drawerContent: @Composable () -> Unit,
464 modifier: Modifier = Modifier,
465 drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
466 gesturesEnabled: Boolean = true,
467 content: @Composable () -> Unit
468 ) {
469 var anchorsInitialized by remember { mutableStateOf(false) }
470 val density = LocalDensity.current
471
472 // TODO Load the motionScheme tokens from the component tokens file
473 val openMotion: FiniteAnimationSpec<Float> = MotionSchemeKeyTokens.DefaultSpatial.value()
474 val closeMotion: FiniteAnimationSpec<Float> = MotionSchemeKeyTokens.FastEffects.value()
475
476 SideEffect {
477 drawerState.density = density
478 drawerState.openDrawerMotionSpec = openMotion
479 drawerState.closeDrawerMotionSpec = closeMotion
480 }
481
482 val scope = rememberCoroutineScope()
483 val navigationMenu = getString(Strings.NavigationMenu)
484
485 val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
486 Box(
487 modifier.anchoredDraggable(
488 state = drawerState.anchoredDraggableState,
489 orientation = Orientation.Horizontal,
490 enabled = gesturesEnabled,
491 reverseDirection = isRtl
492 )
493 ) {
494 Layout(
495 content = {
496 Box(
497 Modifier.semantics {
498 paneTitle = navigationMenu
499 if (drawerState.isOpen) {
500 dismiss {
501 if (
502 drawerState.anchoredDraggableState.confirmValueChange(
503 DrawerValue.Closed
504 )
505 ) {
506 scope.launch { drawerState.close() }
507 }
508 true
509 }
510 }
511 }
512 ) {
513 drawerContent()
514 }
515 Box { content() }
516 }
517 ) { measurables, constraints ->
518 val sheetPlaceable = measurables[0].measure(constraints)
519 val contentPlaceable = measurables[1].measure(constraints)
520 layout(contentPlaceable.width, contentPlaceable.height) {
521 val currentClosedAnchor =
522 drawerState.anchoredDraggableState.anchors.positionOf(DrawerValue.Closed)
523 val calculatedClosedAnchor = -sheetPlaceable.width.toFloat()
524
525 if (!anchorsInitialized || currentClosedAnchor != calculatedClosedAnchor) {
526 if (!anchorsInitialized) {
527 anchorsInitialized = true
528 }
529 drawerState.anchoredDraggableState.updateAnchors(
530 DraggableAnchors {
531 DrawerValue.Closed at calculatedClosedAnchor
532 DrawerValue.Open at 0f
533 }
534 )
535 }
536
537 contentPlaceable.placeRelative(
538 sheetPlaceable.width + drawerState.requireOffset().roundToInt(),
539 0
540 )
541 sheetPlaceable.placeRelative(drawerState.requireOffset().roundToInt(), 0)
542 }
543 }
544 }
545 }
546
547 /**
548 * [Material Design navigation permanent
549 * drawer](https://m3.material.io/components/navigation-drawer/overview)
550 *
551 * Navigation drawers provide ergonomic access to destinations in an app. They’re often next to app
552 * content and affect the screen’s layout grid.
553 *
554 * 
556 *
557 * The permanent navigation drawer is always visible and usually used for frequently switching
558 * destinations. On mobile screens, use [ModalNavigationDrawer] instead.
559 *
560 * @sample androidx.compose.material3.samples.PermanentNavigationDrawerSample
561 * @param drawerContent content inside this drawer
562 * @param modifier the [Modifier] to be applied to this drawer
563 * @param content content of the rest of the UI
564 */
565 @Composable
PermanentNavigationDrawernull566 fun PermanentNavigationDrawer(
567 drawerContent: @Composable () -> Unit,
568 modifier: Modifier = Modifier,
569 content: @Composable () -> Unit
570 ) {
571 Row(modifier.fillMaxSize()) {
572 drawerContent()
573 Box { content() }
574 }
575 }
576
577 /**
578 * Content inside of a modal navigation drawer.
579 *
580 * Note: This version of [ModalDrawerSheet] does not handle back by default. For automatic back
581 * handling and predictive back animations on Android 14+, use the [ModalDrawerSheet] that accepts
582 * `drawerState` as a param.
583 *
584 * @param modifier the [Modifier] to be applied to this drawer's content
585 * @param drawerShape defines the shape of this drawer's container
586 * @param drawerContainerColor the color used for the background of this drawer. Use
587 * [Color.Transparent] to have no color.
588 * @param drawerContentColor the preferred color for content inside this drawer. Defaults to either
589 * the matching content color for [drawerContainerColor], or to the current [LocalContentColor] if
590 * [drawerContainerColor] is not a color from the theme.
591 * @param drawerTonalElevation when [drawerContainerColor] is [ColorScheme.surface], a translucent
592 * primary color overlay is applied on top of the container. A higher tonal elevation value will
593 * result in a darker color in light theme and lighter color in dark theme. See also: [Surface].
594 * @param windowInsets a window insets for the sheet.
595 * @param content content inside of a modal navigation drawer
596 */
597 @Composable
ModalDrawerSheetnull598 fun ModalDrawerSheet(
599 modifier: Modifier = Modifier,
600 drawerShape: Shape = DrawerDefaults.shape,
601 drawerContainerColor: Color = DrawerDefaults.modalContainerColor,
602 drawerContentColor: Color = contentColorFor(drawerContainerColor),
603 drawerTonalElevation: Dp = DrawerDefaults.ModalDrawerElevation,
604 windowInsets: WindowInsets = DrawerDefaults.windowInsets,
605 content: @Composable ColumnScope.() -> Unit
606 ) {
607 DrawerSheet(
608 drawerPredictiveBackState = null,
609 windowInsets = windowInsets,
610 modifier = modifier,
611 drawerShape = drawerShape,
612 drawerContainerColor = drawerContainerColor,
613 drawerContentColor = drawerContentColor,
614 drawerTonalElevation = drawerTonalElevation,
615 content = content
616 )
617 }
618
619 /**
620 * Content inside of a modal navigation drawer.
621 *
622 * Note: This version of [ModalDrawerSheet] requires a [drawerState] to be provided and will handle
623 * back by default for all Android versions, as well as animate during predictive back on Android
624 * 14+.
625 *
626 * @param drawerState state of the drawer
627 * @param modifier the [Modifier] to be applied to this drawer's content
628 * @param drawerShape defines the shape of this drawer's container
629 * @param drawerContainerColor the color used for the background of this drawer. Use
630 * [Color.Transparent] to have no color.
631 * @param drawerContentColor the preferred color for content inside this drawer. Defaults to either
632 * the matching content color for [drawerContainerColor], or to the current [LocalContentColor] if
633 * [drawerContainerColor] is not a color from the theme.
634 * @param drawerTonalElevation when [drawerContainerColor] is [ColorScheme.surface], a translucent
635 * primary color overlay is applied on top of the container. A higher tonal elevation value will
636 * result in a darker color in light theme and lighter color in dark theme. See also: [Surface].
637 * @param windowInsets a window insets for the sheet.
638 * @param content content inside of a modal navigation drawer
639 */
640 @Composable
ModalDrawerSheetnull641 fun ModalDrawerSheet(
642 drawerState: DrawerState,
643 modifier: Modifier = Modifier,
644 drawerShape: Shape = DrawerDefaults.shape,
645 drawerContainerColor: Color = DrawerDefaults.modalContainerColor,
646 drawerContentColor: Color = contentColorFor(drawerContainerColor),
647 drawerTonalElevation: Dp = DrawerDefaults.ModalDrawerElevation,
648 windowInsets: WindowInsets = DrawerDefaults.windowInsets,
649 content: @Composable ColumnScope.() -> Unit
650 ) {
651 DrawerPredictiveBackHandler(drawerState) { drawerPredictiveBackState ->
652 DrawerSheet(
653 drawerPredictiveBackState = drawerPredictiveBackState,
654 windowInsets = windowInsets,
655 modifier = modifier,
656 drawerShape = drawerShape,
657 drawerContainerColor = drawerContainerColor,
658 drawerContentColor = drawerContentColor,
659 drawerTonalElevation = drawerTonalElevation,
660 drawerOffset = { drawerState.anchoredDraggableState.offset },
661 content = content
662 )
663 }
664 }
665
666 /**
667 * Content inside of a dismissible navigation drawer.
668 *
669 * Note: This version of [DismissibleDrawerSheet] does not handle back by default. For automatic
670 * back handling and predictive back animations on Android 14+, use the [DismissibleDrawerSheet]
671 * that accepts `drawerState` as a param.
672 *
673 * @param modifier the [Modifier] to be applied to this drawer's content
674 * @param drawerShape defines the shape of this drawer's container
675 * @param drawerContainerColor the color used for the background of this drawer. Use
676 * [Color.Transparent] to have no color.
677 * @param drawerContentColor the preferred color for content inside this drawer. Defaults to either
678 * the matching content color for [drawerContainerColor], or to the current [LocalContentColor] if
679 * [drawerContainerColor] is not a color from the theme.
680 * @param drawerTonalElevation when [drawerContainerColor] is [ColorScheme.surface], a translucent
681 * primary color overlay is applied on top of the container. A higher tonal elevation value will
682 * result in a darker color in light theme and lighter color in dark theme. See also: [Surface].
683 * @param windowInsets a window insets for the sheet.
684 * @param content content inside of a dismissible navigation drawer
685 */
686 @Composable
DismissibleDrawerSheetnull687 fun DismissibleDrawerSheet(
688 modifier: Modifier = Modifier,
689 drawerShape: Shape = RectangleShape,
690 drawerContainerColor: Color = DrawerDefaults.standardContainerColor,
691 drawerContentColor: Color = contentColorFor(drawerContainerColor),
692 drawerTonalElevation: Dp = DrawerDefaults.DismissibleDrawerElevation,
693 windowInsets: WindowInsets = DrawerDefaults.windowInsets,
694 content: @Composable ColumnScope.() -> Unit
695 ) {
696 DrawerSheet(
697 drawerPredictiveBackState = null,
698 windowInsets = windowInsets,
699 modifier = modifier,
700 drawerShape = drawerShape,
701 drawerContainerColor = drawerContainerColor,
702 drawerContentColor = drawerContentColor,
703 drawerTonalElevation = drawerTonalElevation,
704 content = content
705 )
706 }
707
708 /**
709 * Content inside of a dismissible navigation drawer.
710 *
711 * Note: This version of [DismissibleDrawerSheet] requires a [drawerState] to be provided and will
712 * handle back by default for all Android versions, as well as animate during predictive back on
713 * Android 14+.
714 *
715 * @param drawerState state of the drawer
716 * @param modifier the [Modifier] to be applied to this drawer's content
717 * @param drawerShape defines the shape of this drawer's container
718 * @param drawerContainerColor the color used for the background of this drawer. Use
719 * [Color.Transparent] to have no color.
720 * @param drawerContentColor the preferred color for content inside this drawer. Defaults to either
721 * the matching content color for [drawerContainerColor], or to the current [LocalContentColor] if
722 * [drawerContainerColor] is not a color from the theme.
723 * @param drawerTonalElevation when [drawerContainerColor] is [ColorScheme.surface], a translucent
724 * primary color overlay is applied on top of the container. A higher tonal elevation value will
725 * result in a darker color in light theme and lighter color in dark theme. See also: [Surface].
726 * @param windowInsets a window insets for the sheet.
727 * @param content content inside of a dismissible navigation drawer
728 */
729 @Composable
DismissibleDrawerSheetnull730 fun DismissibleDrawerSheet(
731 drawerState: DrawerState,
732 modifier: Modifier = Modifier,
733 drawerShape: Shape = RectangleShape,
734 drawerContainerColor: Color = DrawerDefaults.standardContainerColor,
735 drawerContentColor: Color = contentColorFor(drawerContainerColor),
736 drawerTonalElevation: Dp = DrawerDefaults.DismissibleDrawerElevation,
737 windowInsets: WindowInsets = DrawerDefaults.windowInsets,
738 content: @Composable ColumnScope.() -> Unit
739 ) {
740 DrawerPredictiveBackHandler(drawerState) { drawerPredictiveBackState ->
741 DrawerSheet(
742 drawerPredictiveBackState = drawerPredictiveBackState,
743 windowInsets = windowInsets,
744 modifier = modifier,
745 drawerShape = drawerShape,
746 drawerContainerColor = drawerContainerColor,
747 drawerContentColor = drawerContentColor,
748 drawerTonalElevation = drawerTonalElevation,
749 drawerOffset = { drawerState.anchoredDraggableState.offset },
750 content = content
751 )
752 }
753 }
754
755 /**
756 * Content inside of a permanent navigation drawer.
757 *
758 * @param modifier the [Modifier] to be applied to this drawer's content
759 * @param drawerShape defines the shape of this drawer's container
760 * @param drawerContainerColor the color used for the background of this drawer. Use
761 * [Color.Transparent] to have no color.
762 * @param drawerContentColor the preferred color for content inside this drawer. Defaults to either
763 * the matching content color for [drawerContainerColor], or to the current [LocalContentColor] if
764 * [drawerContainerColor] is not a color from the theme.
765 * @param drawerTonalElevation when [drawerContainerColor] is [ColorScheme.surface], a translucent
766 * primary color overlay is applied on top of the container. A higher tonal elevation value will
767 * result in a darker color in light theme and lighter color in dark theme. See also: [Surface].
768 * @param windowInsets a window insets for the sheet.
769 * @param content content inside a permanent navigation drawer
770 */
771 @Composable
PermanentDrawerSheetnull772 fun PermanentDrawerSheet(
773 modifier: Modifier = Modifier,
774 drawerShape: Shape = RectangleShape,
775 drawerContainerColor: Color = DrawerDefaults.standardContainerColor,
776 drawerContentColor: Color = contentColorFor(drawerContainerColor),
777 drawerTonalElevation: Dp = DrawerDefaults.PermanentDrawerElevation,
778 windowInsets: WindowInsets = DrawerDefaults.windowInsets,
779 content: @Composable ColumnScope.() -> Unit
780 ) {
781 val navigationMenu = getString(Strings.NavigationMenu)
782 DrawerSheet(
783 drawerPredictiveBackState = null,
784 windowInsets = windowInsets,
785 modifier = modifier.semantics { paneTitle = navigationMenu },
786 drawerShape = drawerShape,
787 drawerContainerColor = drawerContainerColor,
788 drawerContentColor = drawerContentColor,
789 drawerTonalElevation = drawerTonalElevation,
790 content = content
791 )
792 }
793
794 @Composable
DrawerSheetnull795 internal fun DrawerSheet(
796 drawerPredictiveBackState: DrawerPredictiveBackState?,
797 windowInsets: WindowInsets,
798 modifier: Modifier = Modifier,
799 drawerShape: Shape = RectangleShape,
800 drawerContainerColor: Color = DrawerDefaults.standardContainerColor,
801 drawerContentColor: Color = contentColorFor(drawerContainerColor),
802 drawerTonalElevation: Dp = DrawerDefaults.PermanentDrawerElevation,
803 drawerOffset: FloatProducer = FloatProducer { 0F },
804 content: @Composable ColumnScope.() -> Unit
805 ) {
806 val density = LocalDensity.current
807 val maxWidth = NavigationDrawerTokens.ContainerWidth
<lambda>null808 val maxWidthPx = with(density) { maxWidth.toPx() }
809 val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
810 val predictiveBackDrawerContainerModifier =
811 if (drawerPredictiveBackState != null) {
812 Modifier.predictiveBackDrawerContainer(drawerPredictiveBackState, isRtl)
813 } else {
814 Modifier
815 }
816 Surface(
817 modifier =
818 modifier
819 .sizeIn(minWidth = MinimumDrawerWidth, maxWidth = maxWidth)
820 // Scale up the Surface horizontally in case the drawer offset it greater than zero.
821 // This is done to avoid showing a gap when the drawer opens and bounces when it's
822 // applied with a bouncy motion. Note that the content inside the Surface is scaled
823 // back down to maintain its aspect ratio (see below).
824 .horizontalScaleUp(
825 drawerOffset = drawerOffset,
826 drawerWidth = maxWidthPx,
827 isRtl = isRtl
828 )
829 .then(predictiveBackDrawerContainerModifier)
830 .fillMaxHeight(),
831 shape = drawerShape,
832 color = drawerContainerColor,
833 contentColor = drawerContentColor,
834 tonalElevation = drawerTonalElevation
<lambda>null835 ) {
836 val predictiveBackDrawerChildModifier =
837 if (drawerPredictiveBackState != null)
838 Modifier.predictiveBackDrawerChild(drawerPredictiveBackState, isRtl)
839 else Modifier
840 Column(
841 Modifier.sizeIn(minWidth = MinimumDrawerWidth, maxWidth = maxWidth)
842 // Scale the content down in case the drawer offset is greater than one. The
843 // wrapping Surface is scaled up, so this is done to maintain the content's aspect
844 // ratio.
845 .horizontalScaleDown(
846 drawerOffset = drawerOffset,
847 drawerWidth = maxWidthPx,
848 isRtl = isRtl
849 )
850 .then(predictiveBackDrawerChildModifier)
851 .windowInsetsPadding(windowInsets),
852 content = content
853 )
854 }
855 }
856
857 /**
858 * A [Modifier] that scales up the drawing layer on the X axis in case the [drawerOffset] is greater
859 * than zero. The scaling will ensure that there is no visible gap between the drawer and the edge
860 * of the screen in case the drawer bounces when it opens due to a more expressive motion setting.
861 *
862 * A [horizontalScaleDown] should be applied to the content of the drawer to maintain the content
863 * aspect ratio as the container scales up.
864 *
865 * @see horizontalScaleDown
866 */
horizontalScaleUpnull867 private fun Modifier.horizontalScaleUp(
868 drawerOffset: FloatProducer,
869 drawerWidth: Float,
870 isRtl: Boolean
871 ) = graphicsLayer {
872 val offset = drawerOffset()
873 scaleX = if (offset > 0f) 1f + offset / drawerWidth else 1f
874 transformOrigin = TransformOrigin(if (isRtl) 0f else 1f, 0.5f)
875 }
876
877 /**
878 * A [Modifier] that scales down the drawing layer on the X axis in case the [drawerOffset] is
879 * greater than zero. This modifier should be applied to the content inside a component that was
880 * scaled up with a [horizontalScaleUp] modifier. It will ensure that the content maintains its
881 * aspect ratio as the container scales up.
882 *
883 * @see horizontalScaleUp
884 */
Modifiernull885 private fun Modifier.horizontalScaleDown(
886 drawerOffset: FloatProducer,
887 drawerWidth: Float,
888 isRtl: Boolean
889 ) = graphicsLayer {
890 val offset = drawerOffset()
891 scaleX = if (offset > 0f) 1 / (1f + offset / drawerWidth) else 1f
892 transformOrigin = TransformOrigin(if (isRtl) 0f else 1f, 0f)
893 }
894
Modifiernull895 private fun Modifier.predictiveBackDrawerContainer(
896 drawerPredictiveBackState: DrawerPredictiveBackState,
897 isRtl: Boolean
898 ) = graphicsLayer {
899 scaleX = calculatePredictiveBackScaleX(drawerPredictiveBackState)
900 scaleY = calculatePredictiveBackScaleY(drawerPredictiveBackState)
901 transformOrigin = TransformOrigin(if (isRtl) 1f else 0f, 0.5f)
902 }
903
Modifiernull904 private fun Modifier.predictiveBackDrawerChild(
905 drawerPredictiveBackState: DrawerPredictiveBackState,
906 isRtl: Boolean
907 ) = graphicsLayer {
908 // Preserve the original aspect ratio and container alignment of the child
909 // content, and add content margins.
910 val containerScaleX = calculatePredictiveBackScaleX(drawerPredictiveBackState)
911 val containerScaleY = calculatePredictiveBackScaleY(drawerPredictiveBackState)
912 scaleX = if (containerScaleX != 0f) containerScaleY / containerScaleX else 1f
913 transformOrigin = TransformOrigin(if (isRtl) 0f else 1f, 0f)
914 }
915
GraphicsLayerScopenull916 private fun GraphicsLayerScope.calculatePredictiveBackScaleX(
917 drawerPredictiveBackState: DrawerPredictiveBackState
918 ): Float {
919 val width = size.width
920 return if (width.isNaN() || width == 0f) {
921 1f
922 } else {
923 val scaleXDirection = if (drawerPredictiveBackState.swipeEdgeMatchesDrawer) 1 else -1
924 1f + drawerPredictiveBackState.scaleXDistance * scaleXDirection / width
925 }
926 }
927
GraphicsLayerScopenull928 private fun GraphicsLayerScope.calculatePredictiveBackScaleY(
929 drawerPredictiveBackState: DrawerPredictiveBackState
930 ): Float {
931 val height = size.height
932 return if (height.isNaN() || height == 0f) {
933 1f
934 } else {
935 1f - drawerPredictiveBackState.scaleYDistance / height
936 }
937 }
938
939 /**
940 * Registers a [PredictiveBackHandler] and provides animation values in [DrawerPredictiveBackState]
941 * based on back progress.
942 *
943 * @param drawerState state of the drawer
944 * @param content content of the rest of the UI
945 */
946 @Composable
947 internal fun DrawerPredictiveBackHandler(
948 drawerState: DrawerState,
949 content: @Composable (DrawerPredictiveBackState) -> Unit
950 ) {
<lambda>null951 val drawerPredictiveBackState = remember { DrawerPredictiveBackState() }
952 val scope = rememberCoroutineScope()
953 val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
954 val maxScaleXDistanceGrow: Float
955 val maxScaleXDistanceShrink: Float
956 val maxScaleYDistance: Float
<lambda>null957 with(LocalDensity.current) {
958 maxScaleXDistanceGrow = PredictiveBackDrawerMaxScaleXDistanceGrow.toPx()
959 maxScaleXDistanceShrink = PredictiveBackDrawerMaxScaleXDistanceShrink.toPx()
960 maxScaleYDistance = PredictiveBackDrawerMaxScaleYDistance.toPx()
961 }
962
progressnull963 PredictiveBackHandler(enabled = drawerState.isOpen) { progress ->
964 try {
965 progress.collect { backEvent ->
966 drawerPredictiveBackState.update(
967 PredictiveBack.transform(backEvent.progress),
968 backEvent.swipeEdge == BackEventCompat.EDGE_LEFT,
969 isRtl,
970 maxScaleXDistanceGrow,
971 maxScaleXDistanceShrink,
972 maxScaleYDistance
973 )
974 }
975 } catch (e: kotlin.coroutines.cancellation.CancellationException) {
976 drawerPredictiveBackState.clear()
977 } finally {
978 if (drawerPredictiveBackState.swipeEdgeMatchesDrawer) {
979 // If swipe edge matches drawer gravity and we've stretched the drawer horizontally,
980 // un-stretch it smoothly so that it hides completely during the drawer close.
981 scope.launch {
982 animate(
983 initialValue = drawerPredictiveBackState.scaleXDistance,
984 targetValue = 0f
985 ) { value, _ ->
986 drawerPredictiveBackState.scaleXDistance = value
987 }
988 drawerPredictiveBackState.clear()
989 }
990 }
991 drawerState.close()
992 }
993 }
994
<lambda>null995 LaunchedEffect(drawerState.isClosed) {
996 if (drawerState.isClosed) {
997 drawerPredictiveBackState.clear()
998 }
999 }
1000
1001 content(drawerPredictiveBackState)
1002 }
1003
1004 /** Object to hold default values for [ModalNavigationDrawer] */
1005 object DrawerDefaults {
1006 /** Default Elevation for drawer container in the [ModalNavigationDrawer]. */
1007 val ModalDrawerElevation = ElevationTokens.Level0
1008
1009 /** Default Elevation for drawer container in the [PermanentNavigationDrawer]. */
1010 val PermanentDrawerElevation = NavigationDrawerTokens.StandardContainerElevation
1011
1012 /** Default Elevation for drawer container in the [DismissibleNavigationDrawer]. */
1013 val DismissibleDrawerElevation = NavigationDrawerTokens.StandardContainerElevation
1014
1015 /** Default shape for a navigation drawer. */
1016 val shape: Shape
1017 @Composable get() = NavigationDrawerTokens.ContainerShape.value
1018
1019 /** Default color of the scrim that obscures content when the drawer is open */
1020 val scrimColor: Color
1021 @Composable get() = ScrimTokens.ContainerColor.value.copy(ScrimTokens.ContainerOpacity)
1022
1023 /** Default container color for a navigation drawer */
1024 @Deprecated(
1025 message = "Please use standardContainerColor or modalContainerColor instead.",
1026 replaceWith = ReplaceWith("standardContainerColor"),
1027 level = DeprecationLevel.WARNING,
1028 )
1029 val containerColor: Color
1030 @Composable get() = NavigationDrawerTokens.StandardContainerColor.value
1031
1032 /**
1033 * Default container color for a [DismissibleNavigationDrawer] and [PermanentNavigationDrawer]
1034 */
1035 val standardContainerColor: Color
1036 @Composable get() = NavigationDrawerTokens.StandardContainerColor.value
1037
1038 /** Default container color for a [ModalNavigationDrawer] */
1039 val modalContainerColor: Color
1040 @Composable get() = NavigationDrawerTokens.ModalContainerColor.value
1041
1042 /** Default and maximum width of a navigation drawer */
1043 val MaximumDrawerWidth = NavigationDrawerTokens.ContainerWidth
1044
1045 /** Default window insets for drawer sheets */
1046 val windowInsets: WindowInsets
1047 @Composable
1048 get() =
1049 WindowInsets.systemBarsForVisualComponents.only(
1050 WindowInsetsSides.Vertical + WindowInsetsSides.Start
1051 )
1052 }
1053
1054 /**
1055 * Material Design navigation drawer item.
1056 *
1057 * A [NavigationDrawerItem] represents a destination within drawers, either [ModalNavigationDrawer],
1058 * [PermanentNavigationDrawer] or [DismissibleNavigationDrawer].
1059 *
1060 * @sample androidx.compose.material3.samples.ModalNavigationDrawerSample
1061 * @param label text label for this item
1062 * @param selected whether this item is selected
1063 * @param onClick called when this item is clicked
1064 * @param modifier the [Modifier] to be applied to this item
1065 * @param icon optional icon for this item, typically an [Icon]
1066 * @param badge optional badge to show on this item from the end side
1067 * @param shape optional shape for the active indicator
1068 * @param colors [NavigationDrawerItemColors] that will be used to resolve the colors used for this
1069 * item in different states. See [NavigationDrawerItemDefaults.colors].
1070 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
1071 * emitting [Interaction]s for this item. You can use this to change the item's appearance or
1072 * preview the item in different states. Note that if `null` is provided, interactions will still
1073 * happen internally.
1074 */
1075 @Composable
NavigationDrawerItemnull1076 fun NavigationDrawerItem(
1077 label: @Composable () -> Unit,
1078 selected: Boolean,
1079 onClick: () -> Unit,
1080 modifier: Modifier = Modifier,
1081 icon: (@Composable () -> Unit)? = null,
1082 badge: (@Composable () -> Unit)? = null,
1083 shape: Shape = NavigationDrawerTokens.ActiveIndicatorShape.value,
1084 colors: NavigationDrawerItemColors = NavigationDrawerItemDefaults.colors(),
1085 interactionSource: MutableInteractionSource? = null
1086 ) {
1087 Surface(
1088 selected = selected,
1089 onClick = onClick,
1090 modifier =
1091 modifier
1092 .semantics { role = Role.Tab }
1093 .heightIn(min = NavigationDrawerTokens.ActiveIndicatorHeight)
1094 .fillMaxWidth(),
1095 shape = shape,
1096 color = colors.containerColor(selected).value,
1097 interactionSource = interactionSource,
1098 ) {
1099 Row(
1100 Modifier.padding(start = 16.dp, end = 24.dp),
1101 verticalAlignment = Alignment.CenterVertically
1102 ) {
1103 if (icon != null) {
1104 val iconColor = colors.iconColor(selected).value
1105 CompositionLocalProvider(LocalContentColor provides iconColor, content = icon)
1106 Spacer(Modifier.width(12.dp))
1107 }
1108 Box(Modifier.weight(1f)) {
1109 val labelColor = colors.textColor(selected).value
1110 CompositionLocalProvider(LocalContentColor provides labelColor, content = label)
1111 }
1112 if (badge != null) {
1113 Spacer(Modifier.width(12.dp))
1114 val badgeColor = colors.badgeColor(selected).value
1115 CompositionLocalProvider(LocalContentColor provides badgeColor, content = badge)
1116 }
1117 }
1118 }
1119 }
1120
1121 /** Represents the colors of the various elements of a drawer item. */
1122 @Stable
1123 interface NavigationDrawerItemColors {
1124 /**
1125 * Represents the icon color for this item, depending on whether it is [selected].
1126 *
1127 * @param selected whether the item is selected
1128 */
iconColornull1129 @Composable fun iconColor(selected: Boolean): State<Color>
1130
1131 /**
1132 * Represents the text color for this item, depending on whether it is [selected].
1133 *
1134 * @param selected whether the item is selected
1135 */
1136 @Composable fun textColor(selected: Boolean): State<Color>
1137
1138 /**
1139 * Represents the badge color for this item, depending on whether it is [selected].
1140 *
1141 * @param selected whether the item is selected
1142 */
1143 @Composable fun badgeColor(selected: Boolean): State<Color>
1144
1145 /**
1146 * Represents the container color for this item, depending on whether it is [selected].
1147 *
1148 * @param selected whether the item is selected
1149 */
1150 @Composable fun containerColor(selected: Boolean): State<Color>
1151 }
1152
1153 /** Defaults used in [NavigationDrawerItem]. */
1154 object NavigationDrawerItemDefaults {
1155 /**
1156 * Creates a [NavigationDrawerItemColors] with the provided colors according to the Material
1157 * specification.
1158 *
1159 * @param selectedContainerColor the color to use for the background of the item when selected
1160 * @param unselectedContainerColor the color to use for the background of the item when
1161 * unselected
1162 * @param selectedIconColor the color to use for the icon when the item is selected.
1163 * @param unselectedIconColor the color to use for the icon when the item is unselected.
1164 * @param selectedTextColor the color to use for the text label when the item is selected.
1165 * @param unselectedTextColor the color to use for the text label when the item is unselected.
1166 * @param selectedBadgeColor the color to use for the badge when the item is selected.
1167 * @param unselectedBadgeColor the color to use for the badge when the item is unselected.
1168 * @return the resulting [NavigationDrawerItemColors] used for [NavigationDrawerItem]
1169 */
1170 @Composable
1171 fun colors(
1172 selectedContainerColor: Color = NavigationDrawerTokens.ActiveIndicatorColor.value,
1173 unselectedContainerColor: Color = Color.Transparent,
1174 selectedIconColor: Color = NavigationDrawerTokens.ActiveIconColor.value,
1175 unselectedIconColor: Color = NavigationDrawerTokens.InactiveIconColor.value,
1176 selectedTextColor: Color = NavigationDrawerTokens.ActiveLabelTextColor.value,
1177 unselectedTextColor: Color = NavigationDrawerTokens.InactiveLabelTextColor.value,
1178 selectedBadgeColor: Color = selectedTextColor,
1179 unselectedBadgeColor: Color = unselectedTextColor,
1180 ): NavigationDrawerItemColors =
1181 DefaultDrawerItemsColor(
1182 selectedIconColor,
1183 unselectedIconColor,
1184 selectedTextColor,
1185 unselectedTextColor,
1186 selectedContainerColor,
1187 unselectedContainerColor,
1188 selectedBadgeColor,
1189 unselectedBadgeColor
1190 )
1191
1192 /**
1193 * Default external padding for a [NavigationDrawerItem] according to the Material
1194 * specification.
1195 */
1196 val ItemPadding = PaddingValues(horizontal = 12.dp)
1197 }
1198
1199 @Stable
1200 internal class DrawerPredictiveBackState {
1201
1202 var swipeEdgeMatchesDrawer by mutableStateOf(true)
1203
1204 var scaleXDistance by mutableFloatStateOf(0f)
1205
1206 var scaleYDistance by mutableFloatStateOf(0f)
1207
updatenull1208 fun update(
1209 progress: Float,
1210 swipeEdgeLeft: Boolean,
1211 isRtl: Boolean,
1212 maxScaleXDistanceGrow: Float,
1213 maxScaleXDistanceShrink: Float,
1214 maxScaleYDistance: Float
1215 ) {
1216 swipeEdgeMatchesDrawer = swipeEdgeLeft != isRtl
1217 val maxScaleXDistance =
1218 if (swipeEdgeMatchesDrawer) maxScaleXDistanceGrow else maxScaleXDistanceShrink
1219 scaleXDistance = lerp(0f, maxScaleXDistance, progress)
1220 scaleYDistance = lerp(0f, maxScaleYDistance, progress)
1221 }
1222
clearnull1223 fun clear() {
1224 swipeEdgeMatchesDrawer = true
1225 scaleXDistance = 0f
1226 scaleYDistance = 0f
1227 }
1228 }
1229
1230 private class DefaultDrawerItemsColor(
1231 val selectedIconColor: Color,
1232 val unselectedIconColor: Color,
1233 val selectedTextColor: Color,
1234 val unselectedTextColor: Color,
1235 val selectedContainerColor: Color,
1236 val unselectedContainerColor: Color,
1237 val selectedBadgeColor: Color,
1238 val unselectedBadgeColor: Color
1239 ) : NavigationDrawerItemColors {
1240 @Composable
iconColornull1241 override fun iconColor(selected: Boolean): State<Color> {
1242 return rememberUpdatedState(if (selected) selectedIconColor else unselectedIconColor)
1243 }
1244
1245 @Composable
textColornull1246 override fun textColor(selected: Boolean): State<Color> {
1247 return rememberUpdatedState(if (selected) selectedTextColor else unselectedTextColor)
1248 }
1249
1250 @Composable
containerColornull1251 override fun containerColor(selected: Boolean): State<Color> {
1252 return rememberUpdatedState(
1253 if (selected) selectedContainerColor else unselectedContainerColor
1254 )
1255 }
1256
1257 @Composable
badgeColornull1258 override fun badgeColor(selected: Boolean): State<Color> {
1259 return rememberUpdatedState(if (selected) selectedBadgeColor else unselectedBadgeColor)
1260 }
1261
equalsnull1262 override fun equals(other: Any?): Boolean {
1263 if (this === other) return true
1264 if (other !is DefaultDrawerItemsColor) return false
1265
1266 if (selectedIconColor != other.selectedIconColor) return false
1267 if (unselectedIconColor != other.unselectedIconColor) return false
1268 if (selectedTextColor != other.selectedTextColor) return false
1269 if (unselectedTextColor != other.unselectedTextColor) return false
1270 if (selectedContainerColor != other.selectedContainerColor) return false
1271 if (unselectedContainerColor != other.unselectedContainerColor) return false
1272 if (selectedBadgeColor != other.selectedBadgeColor) return false
1273 return unselectedBadgeColor == other.unselectedBadgeColor
1274 }
1275
hashCodenull1276 override fun hashCode(): Int {
1277 var result = selectedIconColor.hashCode()
1278 result = 31 * result + unselectedIconColor.hashCode()
1279 result = 31 * result + selectedTextColor.hashCode()
1280 result = 31 * result + unselectedTextColor.hashCode()
1281 result = 31 * result + selectedContainerColor.hashCode()
1282 result = 31 * result + unselectedContainerColor.hashCode()
1283 result = 31 * result + selectedBadgeColor.hashCode()
1284 result = 31 * result + unselectedBadgeColor.hashCode()
1285 return result
1286 }
1287 }
1288
calculateFractionnull1289 private fun calculateFraction(a: Float, b: Float, pos: Float) =
1290 ((pos - a) / (b - a)).coerceIn(0f, 1f)
1291
1292 @Composable
1293 private fun Scrim(open: Boolean, onClose: () -> Unit, fraction: () -> Float, color: Color) {
1294 val closeDrawer = getString(Strings.CloseDrawer)
1295 val dismissDrawer =
1296 if (open) {
1297 Modifier.pointerInput(onClose) { detectTapGestures { onClose() } }
1298 .semantics(mergeDescendants = true) {
1299 contentDescription = closeDrawer
1300 onClick {
1301 onClose()
1302 true
1303 }
1304 }
1305 } else {
1306 Modifier
1307 }
1308
1309 Canvas(Modifier.fillMaxSize().then(dismissDrawer)) { drawRect(color, alpha = fraction()) }
1310 }
1311
1312 private val DrawerPositionalThreshold = 0.5f
1313 private val DrawerVelocityThreshold = 400.dp
1314 private val MinimumDrawerWidth = 240.dp
1315
1316 internal val PredictiveBackDrawerMaxScaleXDistanceGrow = 12.dp
1317 internal val PredictiveBackDrawerMaxScaleXDistanceShrink = 24.dp
1318 internal val PredictiveBackDrawerMaxScaleYDistance = 48.dp
1319
1320 // TODO: b/177571613 this should be a proper decay settling
1321 // this is taken from the DrawerLayout's DragViewHelper as a min duration.
1322 private val AnchoredDraggableDefaultAnimationSpec = TweenSpec<Float>(durationMillis = 256)
1323