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 * 
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 * 
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