1 /*
<lambda>null2  * Copyright 2019 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.material
18 
19 import androidx.annotation.FloatRange
20 import androidx.compose.animation.core.AnimationSpec
21 import androidx.compose.animation.core.TweenSpec
22 import androidx.compose.animation.core.animateFloatAsState
23 import androidx.compose.foundation.Canvas
24 import androidx.compose.foundation.gestures.Orientation
25 import androidx.compose.foundation.gestures.detectTapGestures
26 import androidx.compose.foundation.layout.Box
27 import androidx.compose.foundation.layout.BoxWithConstraints
28 import androidx.compose.foundation.layout.Column
29 import androidx.compose.foundation.layout.ColumnScope
30 import androidx.compose.foundation.layout.fillMaxSize
31 import androidx.compose.foundation.layout.offset
32 import androidx.compose.foundation.layout.padding
33 import androidx.compose.foundation.layout.sizeIn
34 import androidx.compose.material.BottomDrawerValue.Closed
35 import androidx.compose.material.BottomDrawerValue.Expanded
36 import androidx.compose.material.BottomDrawerValue.Open
37 import androidx.compose.runtime.Composable
38 import androidx.compose.runtime.SideEffect
39 import androidx.compose.runtime.Stable
40 import androidx.compose.runtime.getValue
41 import androidx.compose.runtime.remember
42 import androidx.compose.runtime.rememberCoroutineScope
43 import androidx.compose.runtime.saveable.Saver
44 import androidx.compose.runtime.saveable.rememberSaveable
45 import androidx.compose.ui.Modifier
46 import androidx.compose.ui.geometry.Offset
47 import androidx.compose.ui.graphics.Color
48 import androidx.compose.ui.graphics.Shape
49 import androidx.compose.ui.graphics.isSpecified
50 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
51 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
52 import androidx.compose.ui.input.nestedscroll.nestedScroll
53 import androidx.compose.ui.input.pointer.pointerInput
54 import androidx.compose.ui.layout.onSizeChanged
55 import androidx.compose.ui.platform.LocalDensity
56 import androidx.compose.ui.platform.LocalLayoutDirection
57 import androidx.compose.ui.semantics.contentDescription
58 import androidx.compose.ui.semantics.dismiss
59 import androidx.compose.ui.semantics.onClick
60 import androidx.compose.ui.semantics.paneTitle
61 import androidx.compose.ui.semantics.semantics
62 import androidx.compose.ui.unit.Density
63 import androidx.compose.ui.unit.Dp
64 import androidx.compose.ui.unit.IntOffset
65 import androidx.compose.ui.unit.LayoutDirection
66 import androidx.compose.ui.unit.Velocity
67 import androidx.compose.ui.unit.dp
68 import androidx.compose.ui.util.fastCoerceIn
69 import kotlin.jvm.JvmName
70 import kotlin.math.abs
71 import kotlin.math.max
72 import kotlin.math.min
73 import kotlin.math.roundToInt
74 import kotlinx.coroutines.CancellationException
75 import kotlinx.coroutines.launch
76 
77 /** Possible values of [DrawerState]. */
78 enum class DrawerValue {
79     /** The state of the drawer when it is closed. */
80     Closed,
81 
82     /** The state of the drawer when it is open. */
83     Open
84 }
85 
86 /** Possible values of [BottomDrawerState]. */
87 enum class BottomDrawerValue {
88     /** The state of the bottom drawer when it is closed. */
89     Closed,
90 
91     /** The state of the bottom drawer when it is open (i.e. at 50% height). */
92     Open,
93 
94     /** The state of the bottom drawer when it is expanded (i.e. at 100% height). */
95     Expanded
96 }
97 
98 /**
99  * State of the [ModalDrawer] composable.
100  *
101  * @param initialValue The initial value of the state.
102  * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
103  */
104 @Suppress("NotCloseable")
105 @OptIn(ExperimentalMaterialApi::class)
106 @Stable
107 class DrawerState(
108     initialValue: DrawerValue,
<lambda>null109     confirmStateChange: (DrawerValue) -> Boolean = { true }
110 ) {
111 
112     internal val anchoredDraggableState =
113         AnchoredDraggableState(
114             initialValue = initialValue,
115             animationSpec = AnimationSpec,
116             confirmValueChange = confirmStateChange,
<lambda>null117             positionalThreshold = { with(requireDensity()) { DrawerPositionalThreshold.toPx() } },
<lambda>null118             velocityThreshold = { with(requireDensity()) { DrawerVelocityThreshold.toPx() } },
119         )
120 
121     /** Whether the drawer is open. */
122     val isOpen: Boolean
123         get() = currentValue == DrawerValue.Open
124 
125     /** Whether the drawer is closed. */
126     val isClosed: Boolean
127         get() = currentValue == DrawerValue.Closed
128 
129     /**
130      * The current value of the state.
131      *
132      * If no swipe or animation is in progress, this corresponds to the start the drawer currently
133      * in. If a swipe or an animation is in progress, this corresponds the state drawer was in
134      * before the swipe or animation started.
135      */
136     val currentValue: DrawerValue
137         get() {
138             return anchoredDraggableState.currentValue
139         }
140 
141     /** Whether the state is currently animating. */
142     val isAnimationRunning: Boolean
143         get() {
144             return anchoredDraggableState.isAnimationRunning
145         }
146 
147     /**
148      * Open the drawer with animation and suspend until it if fully opened or animation has been
149      * cancelled. This method will throw [CancellationException] if the animation is interrupted
150      *
151      * @return the reason the open animation ended
152      */
opennull153     suspend fun open() = anchoredDraggableState.animateTo(DrawerValue.Open)
154 
155     /**
156      * Close the drawer with animation and suspend until it if fully closed or animation has been
157      * cancelled. This method will throw [CancellationException] if the animation is interrupted
158      *
159      * @return the reason the close animation ended
160      */
161     suspend fun close() = anchoredDraggableState.animateTo(DrawerValue.Closed)
162 
163     /**
164      * Set the state of the drawer with specific animation
165      *
166      * @param targetValue The new value to animate to.
167      * @param anim Set the state of the drawer with specific animation
168      */
169     @ExperimentalMaterialApi
170     @Deprecated(
171         message =
172             "This method has been replaced by the open and close methods. The animation " +
173                 "spec is now an implementation detail of ModalDrawer.",
174         level = DeprecationLevel.ERROR
175     )
176     suspend fun animateTo(
177         targetValue: DrawerValue,
178         @Suppress("UNUSED_PARAMETER") anim: AnimationSpec<Float>
179     ) {
180         anchoredDraggableState.animateTo(targetValue)
181     }
182 
183     /**
184      * Set the state without any animation and suspend until it's set
185      *
186      * @param targetValue The new target value
187      */
snapTonull188     suspend fun snapTo(targetValue: DrawerValue) {
189         anchoredDraggableState.snapTo(targetValue)
190     }
191 
192     /**
193      * The target value of the drawer state.
194      *
195      * If a swipe is in progress, this is the value that the Drawer would animate to if the swipe
196      * finishes. If an animation is running, this is the target value of that animation. Finally, if
197      * no swipe or animation is in progress, this is the same as the [currentValue].
198      */
199     @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
200     @ExperimentalMaterialApi
201     @get:ExperimentalMaterialApi
202     val targetValue: DrawerValue
203         get() = anchoredDraggableState.targetValue
204 
205     /**
206      * The current position (in pixels) of the drawer sheet, or [Float.NaN] before the offset is
207      * initialized.
208      *
209      * @see [AnchoredDraggableState.offset] for more information.
210      */
211     @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
212     @ExperimentalMaterialApi
213     @get:ExperimentalMaterialApi
214     val offset: Float
215         get() = anchoredDraggableState.offset
216 
requireOffsetnull217     internal fun requireOffset(): Float = anchoredDraggableState.requireOffset()
218 
219     internal var density: Density? = null
220 
221     private fun requireDensity() =
222         requireNotNull(density) {
223             "The density on DrawerState ($this) was not set. Did you use DrawerState with the Drawer " +
224                 "composable?"
225         }
226 
227     companion object {
228         /** The default [Saver] implementation for [DrawerState]. */
Savernull229         fun Saver(confirmStateChange: (DrawerValue) -> Boolean) =
230             Saver<DrawerState, DrawerValue>(
231                 save = { it.currentValue },
<lambda>null232                 restore = { DrawerState(it, confirmStateChange) }
233             )
234     }
235 }
236 
237 /**
238  * State of the [BottomDrawer] composable.
239  *
240  * @param initialValue The initial value of the state.
241  * @param density The density that this state can use to convert values to and from dp.
242  * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
243  * @param animationSpec The animation spec to be used for open/close animations, as well as settling
244  *   when a user lets go.
245  */
246 @OptIn(ExperimentalMaterialApi::class)
247 @Suppress("NotCloseable")
248 class BottomDrawerState(
249     initialValue: BottomDrawerValue,
250     density: Density,
<lambda>null251     confirmStateChange: (BottomDrawerValue) -> Boolean = { true },
252     animationSpec: AnimationSpec<Float> = DrawerDefaults.AnimationSpec
253 ) {
254     internal val anchoredDraggableState =
255         AnchoredDraggableState(
256             initialValue = initialValue,
257             animationSpec = animationSpec,
258             confirmValueChange = confirmStateChange,
<lambda>null259             positionalThreshold = { with(density) { DrawerPositionalThreshold.toPx() } },
<lambda>null260             velocityThreshold = { with(density) { DrawerVelocityThreshold.toPx() } },
261         )
262 
263     /**
264      * The target value the state will settle at once the current interaction ends, or the
265      * [currentValue] if there is no interaction in progress.
266      */
267     val targetValue: BottomDrawerValue
268         get() = anchoredDraggableState.targetValue
269 
270     /** The current offset in pixels, or [Float.NaN] if it has not been initialized yet. */
271     val offset: Float
272         get() = anchoredDraggableState.offset
273 
requireOffsetnull274     internal fun requireOffset(): Float = anchoredDraggableState.requireOffset()
275 
276     /** The current value of the [BottomDrawerState]. */
277     val currentValue: BottomDrawerValue
278         get() = anchoredDraggableState.currentValue
279 
280     /** Whether the drawer is open, either in opened or expanded state. */
281     val isOpen: Boolean
282         get() = anchoredDraggableState.currentValue != Closed
283 
284     /** Whether the drawer is closed. */
285     val isClosed: Boolean
286         get() = anchoredDraggableState.currentValue == Closed
287 
288     /** Whether the drawer is expanded. */
289     val isExpanded: Boolean
290         get() = anchoredDraggableState.currentValue == Expanded
291 
292     /**
293      * The fraction of the progress, within [0f..1f] bounds, or 1f if the [AnchoredDraggableState]
294      * is in a settled state.
295      */
296     @Deprecated(
297         message = "Please use the progress function to query progress explicitly between targets.",
298         replaceWith = ReplaceWith("progress(from = , to = )")
299     ) // TODO: Remove in the future b/323882175
300     @get:FloatRange(from = 0.0, to = 1.0)
301     @ExperimentalMaterialApi
302     val progress: Float
303         get() = anchoredDraggableState.progress
304 
305     /**
306      * The fraction of the offset between [from] and [to], as a fraction between [0f..1f], or 1f if
307      * [from] is equal to [to].
308      *
309      * @param from The starting value used to calculate the distance
310      * @param to The end value used to calculate the distance
311      */
312     @FloatRange(from = 0.0, to = 1.0)
313     fun progress(from: BottomDrawerValue, to: BottomDrawerValue): Float {
314         val fromOffset = anchoredDraggableState.anchors.positionOf(from)
315         val toOffset = anchoredDraggableState.anchors.positionOf(to)
316         val currentOffset =
317             anchoredDraggableState.offset.coerceIn(
318                 min(fromOffset, toOffset), // fromOffset might be > toOffset
319                 max(fromOffset, toOffset)
320             )
321         val fraction = (currentOffset - fromOffset) / (toOffset - fromOffset)
322         return if (fraction.isNaN()) 1f else abs(fraction)
323     }
324 
325     /**
326      * Open the drawer with animation and suspend until it if fully opened or animation has been
327      * cancelled. If the content height is less than [BottomDrawerOpenFraction], the drawer state
328      * will move to [BottomDrawerValue.Expanded] instead.
329      *
330      * @throws [CancellationException] if the animation is interrupted
331      */
opennull332     suspend fun open() {
333         val targetValue = if (isOpenEnabled) Open else Expanded
334         anchoredDraggableState.animateTo(targetValue)
335     }
336 
337     /**
338      * Close the drawer with animation and suspend until it if fully closed or animation has been
339      * cancelled.
340      *
341      * @throws [CancellationException] if the animation is interrupted
342      */
closenull343     suspend fun close() = anchoredDraggableState.animateTo(Closed)
344 
345     /**
346      * Expand the drawer with animation and suspend until it if fully expanded or animation has been
347      * cancelled.
348      *
349      * @throws [CancellationException] if the animation is interrupted
350      */
351     suspend fun expand() = anchoredDraggableState.animateTo(Expanded)
352 
353     internal suspend fun animateTo(
354         target: BottomDrawerValue,
355         velocity: Float = anchoredDraggableState.lastVelocity
356     ) = anchoredDraggableState.animateTo(target, velocity)
357 
358     internal suspend fun snapTo(target: BottomDrawerValue) = anchoredDraggableState.snapTo(target)
359 
360     internal fun confirmStateChange(value: BottomDrawerValue): Boolean =
361         anchoredDraggableState.confirmValueChange(value)
362 
363     private val isOpenEnabled: Boolean
364         get() = anchoredDraggableState.anchors.hasAnchorFor(Open)
365 
366     internal val nestedScrollConnection =
367         ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(anchoredDraggableState)
368 
369     internal var density: Density? = null
370 
371     companion object {
372         /** The default [Saver] implementation for [BottomDrawerState]. */
373         fun Saver(
374             density: Density,
375             confirmStateChange: (BottomDrawerValue) -> Boolean,
376             animationSpec: AnimationSpec<Float>
377         ) =
378             Saver<BottomDrawerState, BottomDrawerValue>(
379                 save = { it.anchoredDraggableState.currentValue },
380                 restore = { BottomDrawerState(it, density, confirmStateChange, animationSpec) }
381             )
382     }
383 }
384 
385 /**
386  * Create and [remember] a [DrawerState].
387  *
388  * @param initialValue The initial value of the state.
389  * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
390  */
391 @Composable
rememberDrawerStatenull392 fun rememberDrawerState(
393     initialValue: DrawerValue,
394     confirmStateChange: (DrawerValue) -> Boolean = { true }
395 ): DrawerState {
<lambda>null396     return rememberSaveable(saver = DrawerState.Saver(confirmStateChange)) {
397         DrawerState(initialValue, confirmStateChange)
398     }
399 }
400 
401 /**
402  * Create and [remember] a [BottomDrawerState].
403  *
404  * @param initialValue The initial value of the state.
405  * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
406  * @param animationSpec The animation spec to be used for open/close animations, as well as settling
407  *   when a user lets go.
408  */
409 @Composable
rememberBottomDrawerStatenull410 fun rememberBottomDrawerState(
411     initialValue: BottomDrawerValue,
412     confirmStateChange: (BottomDrawerValue) -> Boolean = { true },
413     animationSpec: AnimationSpec<Float> = DrawerDefaults.AnimationSpec,
414 ): BottomDrawerState {
415     val density = LocalDensity.current
416     return rememberSaveable(
417         density,
418         saver = BottomDrawerState.Saver(density, confirmStateChange, animationSpec)
<lambda>null419     ) {
420         BottomDrawerState(initialValue, density, confirmStateChange, animationSpec)
421     }
422 }
423 
424 /**
425  * [Material Design modal navigation
426  * drawer](https://material.io/components/navigation-drawer#modal-drawer)
427  *
428  * Modal navigation drawers block interaction with the rest of an app’s content with a scrim. They
429  * are elevated above most of the app’s UI and don’t affect the screen’s layout grid.
430  *
431  * ![Modal drawer
432  * image](https://developer.android.com/images/reference/androidx/compose/material/modal-drawer.png)
433  *
434  * See [BottomDrawer] for a layout that introduces a bottom drawer, suitable when using bottom
435  * navigation.
436  *
437  * @sample androidx.compose.material.samples.ModalDrawerSample
438  * @param drawerContent composable that represents content inside the drawer
439  * @param modifier optional modifier for the drawer
440  * @param drawerState state of the drawer
441  * @param gesturesEnabled whether or not drawer can be interacted by gestures
442  * @param drawerShape shape of the drawer sheet
443  * @param drawerElevation drawer sheet elevation. This controls the size of the shadow below the
444  *   drawer sheet
445  * @param drawerBackgroundColor background color to be used for the drawer sheet
446  * @param drawerContentColor color of the content to use inside the drawer sheet. Defaults to either
447  *   the matching content color for [drawerBackgroundColor], or, if it is not a color from the
448  *   theme, this will keep the same value set above this Surface.
449  * @param scrimColor color of the scrim that obscures content when the drawer is open
450  * @param content content of the rest of the UI
451  * @throws IllegalStateException when parent has [Float.POSITIVE_INFINITY] width
452  */
453 @Composable
454 @OptIn(ExperimentalMaterialApi::class)
ModalDrawernull455 fun ModalDrawer(
456     drawerContent: @Composable ColumnScope.() -> Unit,
457     modifier: Modifier = Modifier,
458     drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
459     gesturesEnabled: Boolean = true,
460     drawerShape: Shape = DrawerDefaults.shape,
461     drawerElevation: Dp = DrawerDefaults.Elevation,
462     drawerBackgroundColor: Color = DrawerDefaults.backgroundColor,
463     drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
464     scrimColor: Color = DrawerDefaults.scrimColor,
465     content: @Composable () -> Unit
466 ) {
467     val scope = rememberCoroutineScope()
468     BoxWithConstraints(modifier.fillMaxSize()) {
469         val modalDrawerConstraints = constraints
470         // TODO : think about Infinite max bounds case
471         if (!modalDrawerConstraints.hasBoundedWidth) {
472             throw IllegalStateException("Drawer shouldn't have infinite width")
473         }
474         val minValue = -modalDrawerConstraints.maxWidth.toFloat()
475         val maxValue = 0f
476 
477         val density = LocalDensity.current
478         SideEffect {
479             drawerState.density = density
480             val anchors = DraggableAnchors {
481                 DrawerValue.Closed at minValue
482                 DrawerValue.Open at maxValue
483             }
484             drawerState.anchoredDraggableState.updateAnchors(anchors)
485         }
486 
487         val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
488         Box(
489             Modifier.anchoredDraggable(
490                 state = drawerState.anchoredDraggableState,
491                 orientation = Orientation.Horizontal,
492                 enabled = gesturesEnabled,
493                 reverseDirection = isRtl
494             )
495         ) {
496             Box { content() }
497             Scrim(
498                 open = drawerState.isOpen,
499                 onClose = {
500                     if (
501                         gesturesEnabled &&
502                             drawerState.anchoredDraggableState.confirmValueChange(
503                                 DrawerValue.Closed
504                             )
505                     ) {
506                         scope.launch { drawerState.close() }
507                     }
508                 },
509                 fraction = { calculateFraction(minValue, maxValue, drawerState.requireOffset()) },
510                 color = scrimColor
511             )
512             val navigationMenu = getString(Strings.NavigationMenu)
513             Surface(
514                 modifier =
515                     with(LocalDensity.current) {
516                             Modifier.sizeIn(
517                                 minWidth = modalDrawerConstraints.minWidth.toDp(),
518                                 minHeight = modalDrawerConstraints.minHeight.toDp(),
519                                 maxWidth = modalDrawerConstraints.maxWidth.toDp(),
520                                 maxHeight = modalDrawerConstraints.maxHeight.toDp()
521                             )
522                         }
523                         .offset { IntOffset(drawerState.requireOffset().roundToInt(), 0) }
524                         .padding(end = EndDrawerPadding)
525                         .semantics {
526                             paneTitle = navigationMenu
527                             if (drawerState.isOpen) {
528                                 dismiss {
529                                     if (
530                                         drawerState.anchoredDraggableState.confirmValueChange(
531                                             DrawerValue.Closed
532                                         )
533                                     ) {
534                                         scope.launch { drawerState.close() }
535                                     }
536                                     true
537                                 }
538                             }
539                         },
540                 shape = drawerShape,
541                 color = drawerBackgroundColor,
542                 contentColor = drawerContentColor,
543                 elevation = drawerElevation
544             ) {
545                 Column(Modifier.fillMaxSize(), content = drawerContent)
546             }
547         }
548     }
549 }
550 
551 /**
552  * [Material Design bottom navigation
553  * drawer](https://material.io/components/navigation-drawer#bottom-drawer)
554  *
555  * Bottom navigation drawers are modal drawers that are anchored to the bottom of the screen instead
556  * of the left or right edge. They are only used with bottom app bars.
557  *
558  * ![Bottom drawer
559  * image](https://developer.android.com/images/reference/androidx/compose/material/bottom-drawer.png)
560  *
561  * See [ModalDrawer] for a layout that introduces a classic from-the-side drawer.
562  *
563  * @sample androidx.compose.material.samples.BottomDrawerSample
564  * @param drawerContent composable that represents content inside the drawer
565  * @param modifier optional [Modifier] for the entire component
566  * @param drawerState state of the drawer
567  * @param gesturesEnabled whether or not drawer can be interacted by gestures
568  * @param drawerShape shape of the drawer sheet
569  * @param drawerElevation drawer sheet elevation. This controls the size of the shadow below the
570  *   drawer sheet
571  * @param drawerBackgroundColor background color to be used for the drawer sheet
572  * @param drawerContentColor color of the content to use inside the drawer sheet. Defaults to either
573  *   the matching content color for [drawerBackgroundColor], or, if it is not a color from the
574  *   theme, this will keep the same value set above this Surface.
575  * @param scrimColor color of the scrim that obscures content when the drawer is open. If the color
576  *   passed is [Color.Unspecified], then a scrim will no longer be applied and the bottom drawer
577  *   will not block interaction with the rest of the screen when visible.
578  * @param content content of the rest of the UI
579  */
580 @OptIn(ExperimentalMaterialApi::class)
581 @Composable
BottomDrawernull582 fun BottomDrawer(
583     drawerContent: @Composable ColumnScope.() -> Unit,
584     modifier: Modifier = Modifier,
585     drawerState: BottomDrawerState = rememberBottomDrawerState(Closed),
586     gesturesEnabled: Boolean = true,
587     drawerShape: Shape = DrawerDefaults.shape,
588     drawerElevation: Dp = DrawerDefaults.Elevation,
589     drawerBackgroundColor: Color = DrawerDefaults.backgroundColor,
590     drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
591     scrimColor: Color = DrawerDefaults.scrimColor,
592     content: @Composable () -> Unit
593 ) {
594     val scope = rememberCoroutineScope()
595     BoxWithConstraints(modifier.fillMaxSize()) {
596         val fullHeight = constraints.maxHeight.toFloat()
597         val isLandscape = constraints.maxWidth > constraints.maxHeight
598         val drawerConstraints =
599             with(LocalDensity.current) {
600                 Modifier.sizeIn(
601                     maxWidth = constraints.maxWidth.toDp(),
602                     maxHeight = constraints.maxHeight.toDp()
603                 )
604             }
605         val nestedScroll =
606             if (gesturesEnabled) {
607                 Modifier.nestedScroll(drawerState.nestedScrollConnection)
608             } else {
609                 Modifier
610             }
611         val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
612 
613         val swipeable =
614             Modifier.then(nestedScroll)
615                 .anchoredDraggable(
616                     state = drawerState.anchoredDraggableState,
617                     orientation = Orientation.Vertical,
618                     enabled = gesturesEnabled,
619                     reverseDirection = isRtl
620                 )
621 
622         Box(swipeable) {
623             content()
624             BottomDrawerScrim(
625                 color = scrimColor,
626                 onDismiss = {
627                     if (gesturesEnabled && drawerState.confirmStateChange(Closed)) {
628                         scope.launch { drawerState.close() }
629                     }
630                 },
631                 visible = drawerState.targetValue != Closed
632             )
633             val navigationMenu = getString(Strings.NavigationMenu)
634             Surface(
635                 drawerConstraints
636                     .onSizeChanged { drawerSize ->
637                         val drawerHeight = drawerSize.height.toFloat()
638                         val newAnchors = DraggableAnchors {
639                             Closed at fullHeight
640                             val peekHeight = fullHeight * BottomDrawerOpenFraction
641                             if (drawerHeight > peekHeight || isLandscape) {
642                                 Open at peekHeight
643                             }
644                             if (drawerHeight > 0f) {
645                                 Expanded at max(0f, fullHeight - drawerHeight)
646                             }
647                         }
648                         // If we are setting the anchors for the first time and have an anchor for
649                         // the current (initial) value, prefer that
650                         val hasAnchors = drawerState.anchoredDraggableState.anchors.size > 0
651                         val newTarget =
652                             if (!hasAnchors && newAnchors.hasAnchorFor(drawerState.currentValue)) {
653                                 drawerState.currentValue
654                             } else {
655                                 when (drawerState.targetValue) {
656                                     Closed -> Closed
657                                     Open,
658                                     Expanded -> {
659                                         val hasHalfExpandedState = newAnchors.hasAnchorFor(Open)
660                                         val newTarget =
661                                             if (hasHalfExpandedState) {
662                                                 Open
663                                             } else {
664                                                 if (newAnchors.hasAnchorFor(Expanded)) Expanded
665                                                 else Closed
666                                             }
667                                         newTarget
668                                     }
669                                 }
670                             }
671                         drawerState.anchoredDraggableState.updateAnchors(newAnchors, newTarget)
672                     }
673                     .offset { IntOffset(x = 0, y = drawerState.requireOffset().roundToInt()) }
674                     .semantics {
675                         paneTitle = navigationMenu
676                         if (drawerState.isOpen) {
677                             // TODO(b/180101663) The action currently doesn't return the correct
678                             // results
679                             dismiss {
680                                 if (drawerState.confirmStateChange(Closed)) {
681                                     scope.launch { drawerState.close() }
682                                 }
683                                 true
684                             }
685                         }
686                     },
687                 shape = drawerShape,
688                 color = drawerBackgroundColor,
689                 contentColor = drawerContentColor,
690                 elevation = drawerElevation
691             ) {
692                 Column(content = drawerContent)
693             }
694         }
695     }
696 }
697 
698 /** Object to hold default values for [ModalDrawer] and [BottomDrawer] */
699 object DrawerDefaults {
700 
701     /**
702      * Default animation spec used for [ModalDrawer] and [BottomDrawer] open and close animations,
703      * as well as settling when a user lets go.
704      */
705     val AnimationSpec = TweenSpec<Float>(durationMillis = 256)
706 
707     /** Default background color for drawer sheets */
708     val backgroundColor: Color
709         @Composable get() = MaterialTheme.colors.surface
710 
711     /** Default elevation for drawer sheet as specified in material specs */
712     val Elevation = 16.dp
713 
714     /** Default shape for drawer sheets */
715     val shape: Shape
716         @Composable get() = MaterialTheme.shapes.large
717 
718     /** Default color of the scrim that obscures content when the drawer is open */
719     val scrimColor: Color
720         @Composable get() = MaterialTheme.colors.onSurface.copy(alpha = ScrimOpacity)
721 
722     /** Default alpha for scrim color */
723     const val ScrimOpacity = 0.32f
724 }
725 
calculateFractionnull726 private fun calculateFraction(a: Float, b: Float, pos: Float) =
727     ((pos - a) / (b - a)).fastCoerceIn(0f, 1f)
728 
729 @Composable
730 private fun BottomDrawerScrim(color: Color, onDismiss: () -> Unit, visible: Boolean) {
731     if (color.isSpecified) {
732         val alpha by
733             animateFloatAsState(targetValue = if (visible) 1f else 0f, animationSpec = TweenSpec())
734         val closeDrawer = getString(Strings.CloseDrawer)
735         val dismissModifier =
736             if (visible) {
737                 Modifier.pointerInput(onDismiss) { detectTapGestures { onDismiss() } }
738                     .semantics(mergeDescendants = true) {
739                         contentDescription = closeDrawer
740                         onClick {
741                             onDismiss()
742                             true
743                         }
744                     }
745             } else {
746                 Modifier
747             }
748 
749         Canvas(Modifier.fillMaxSize().then(dismissModifier)) {
750             drawRect(color = color, alpha = alpha)
751         }
752     }
753 }
754 
755 @Composable
Scrimnull756 private fun Scrim(open: Boolean, onClose: () -> Unit, fraction: () -> Float, color: Color) {
757     val closeDrawer = getString(Strings.CloseDrawer)
758     val dismissDrawer =
759         if (open) {
760             Modifier.pointerInput(onClose) { detectTapGestures { onClose() } }
761                 .semantics(mergeDescendants = true) {
762                     contentDescription = closeDrawer
763                     onClick {
764                         onClose()
765                         true
766                     }
767                 }
768         } else {
769             Modifier
770         }
771 
772     Canvas(Modifier.fillMaxSize().then(dismissDrawer)) { drawRect(color, alpha = fraction()) }
773 }
774 
775 private val EndDrawerPadding = 56.dp
776 private val DrawerPositionalThreshold = 56.dp
777 private val DrawerVelocityThreshold = 400.dp
778 
779 // TODO: b/177571613 this should be a proper decay settling
780 // this is taken from the DrawerLayout's DragViewHelper as a min duration.
781 private val AnimationSpec = TweenSpec<Float>(durationMillis = 256)
782 
783 private const val BottomDrawerOpenFraction = 0.5f
784 
785 @OptIn(ExperimentalMaterialApi::class)
ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnectionnull786 private fun ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
787     state: AnchoredDraggableState<*>
788 ): NestedScrollConnection =
789     object : NestedScrollConnection {
790         val orientation: Orientation = Orientation.Vertical
791 
792         override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
793             val delta = available.toFloat()
794             return if (delta < 0 && source == NestedScrollSource.UserInput) {
795                 state.dispatchRawDelta(delta).toOffset()
796             } else {
797                 Offset.Zero
798             }
799         }
800 
801         override fun onPostScroll(
802             consumed: Offset,
803             available: Offset,
804             source: NestedScrollSource
805         ): Offset {
806             return if (source == NestedScrollSource.UserInput) {
807                 state.dispatchRawDelta(available.toFloat()).toOffset()
808             } else {
809                 Offset.Zero
810             }
811         }
812 
813         override suspend fun onPreFling(available: Velocity): Velocity {
814             val toFling = available.toFloat()
815             val currentOffset = state.requireOffset()
816             return if (toFling < 0 && currentOffset > state.anchors.minAnchor()) {
817                 state.settle(velocity = toFling)
818                 // since we go to the anchor with tween settling, consume all for the best UX
819                 available
820             } else {
821                 Velocity.Zero
822             }
823         }
824 
825         override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
826             state.settle(velocity = available.toFloat())
827             return available
828         }
829 
830         private fun Float.toOffset(): Offset =
831             Offset(
832                 x = if (orientation == Orientation.Horizontal) this else 0f,
833                 y = if (orientation == Orientation.Vertical) this else 0f
834             )
835 
836         @JvmName("velocityToFloat")
837         private fun Velocity.toFloat() = if (orientation == Orientation.Horizontal) x else y
838 
839         @JvmName("offsetToFloat")
840         private fun Offset.toFloat(): Float = if (orientation == Orientation.Horizontal) x else y
841     }
842