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  * ![Navigation drawer
318  * image](https://developer.android.com/images/reference/androidx/compose/material3/navigation-drawer.png)
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  * ![Navigation drawer
448  * image](https://developer.android.com/images/reference/androidx/compose/material3/navigation-drawer.png)
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  * ![Navigation drawer
555  * image](https://developer.android.com/images/reference/androidx/compose/material3/navigation-drawer.png)
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