1 /*
<lambda>null2  * Copyright 2020 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.FastOutSlowInEasing
22 import androidx.compose.animation.core.TweenSpec
23 import androidx.compose.animation.core.animateFloatAsState
24 import androidx.compose.animation.core.tween
25 import androidx.compose.foundation.Canvas
26 import androidx.compose.foundation.gestures.Orientation
27 import androidx.compose.foundation.gestures.detectTapGestures
28 import androidx.compose.foundation.layout.Box
29 import androidx.compose.foundation.layout.Column
30 import androidx.compose.foundation.layout.fillMaxSize
31 import androidx.compose.foundation.layout.padding
32 import androidx.compose.foundation.shape.CornerSize
33 import androidx.compose.material.BackdropValue.Concealed
34 import androidx.compose.material.BackdropValue.Revealed
35 import androidx.compose.runtime.Composable
36 import androidx.compose.runtime.SideEffect
37 import androidx.compose.runtime.Stable
38 import androidx.compose.runtime.getValue
39 import androidx.compose.runtime.remember
40 import androidx.compose.runtime.rememberCoroutineScope
41 import androidx.compose.runtime.saveable.Saver
42 import androidx.compose.runtime.saveable.rememberSaveable
43 import androidx.compose.ui.Alignment
44 import androidx.compose.ui.Modifier
45 import androidx.compose.ui.geometry.Offset
46 import androidx.compose.ui.graphics.Color
47 import androidx.compose.ui.graphics.Shape
48 import androidx.compose.ui.graphics.graphicsLayer
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.SubcomposeLayout
55 import androidx.compose.ui.layout.layout
56 import androidx.compose.ui.platform.LocalDensity
57 import androidx.compose.ui.semantics.collapse
58 import androidx.compose.ui.semantics.expand
59 import androidx.compose.ui.semantics.semantics
60 import androidx.compose.ui.unit.Constraints
61 import androidx.compose.ui.unit.Density
62 import androidx.compose.ui.unit.Dp
63 import androidx.compose.ui.unit.Velocity
64 import androidx.compose.ui.unit.dp
65 import androidx.compose.ui.unit.offset
66 import androidx.compose.ui.util.fastCoerceIn
67 import androidx.compose.ui.util.fastForEach
68 import androidx.compose.ui.util.fastMap
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 [BackdropScaffoldState]. */
78 enum class BackdropValue {
79     /** Indicates the back layer is concealed and the front layer is active. */
80     Concealed,
81 
82     /** Indicates the back layer is revealed and the front layer is inactive. */
83     Revealed
84 }
85 
86 /**
87  * State of the persistent bottom sheet in [BackdropScaffold].
88  *
89  * @param initialValue The initial value of the state.
90  * @param density The density that this state can use to convert values to and from dp.
91  * @param animationSpec The default animation that will be used to animate to a new state.
92  * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
93  * @param snackbarHostState The [SnackbarHostState] used to show snackbars inside the scaffold.
94  */
95 @Suppress("Deprecation")
96 @Stable
BackdropScaffoldStatenull97 fun BackdropScaffoldState(
98     initialValue: BackdropValue,
99     density: Density,
100     animationSpec: AnimationSpec<Float> = BackdropScaffoldDefaults.AnimationSpec,
101     confirmValueChange: (BackdropValue) -> Boolean = { true },
102     snackbarHostState: SnackbarHostState = SnackbarHostState(),
103 ) =
<lambda>null104     BackdropScaffoldState(initialValue, animationSpec, confirmValueChange, snackbarHostState).also {
105         it.density = density
106     }
107 
108 /**
109  * State of the [BackdropScaffold] composable.
110  *
111  * @param initialValue The initial value of the state.
112  * @param animationSpec The default animation that will be used to animate to a new state.
113  * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
114  * @param snackbarHostState The [SnackbarHostState] used to show snackbars inside the scaffold.
115  */
116 @OptIn(ExperimentalMaterialApi::class)
117 @Stable
118 class BackdropScaffoldState
119 @Deprecated(
120     "This constructor is deprecated. Density must be provided by the component. " +
121         "Please use the constructor that provides a [Density].",
122     ReplaceWith(
123         """
124             BackdropScaffoldState(
125                 initialValue = initialValue,
126                 density = LocalDensity.current,
127                 animationSpec = animationSpec,
128                 confirmValueChange = confirmValueChange
129             )
130             """
131     )
132 )
133 constructor(
134     initialValue: BackdropValue,
135     animationSpec: AnimationSpec<Float> = BackdropScaffoldDefaults.AnimationSpec,
<lambda>null136     val confirmValueChange: (BackdropValue) -> Boolean = { true },
137     val snackbarHostState: SnackbarHostState = SnackbarHostState(),
138 ) {
139     /** The current value of the [BottomSheetState]. */
140     val currentValue: BackdropValue
141         get() = anchoredDraggableState.currentValue
142 
143     /**
144      * The target value the state will settle at once the current interaction ends, or the
145      * [currentValue] if there is no interaction in progress.
146      */
147     val targetValue: BackdropValue
148         get() = anchoredDraggableState.targetValue
149 
150     /**
151      * Require the current offset.
152      *
153      * @throws IllegalStateException If the offset has not been initialized yet
154      */
requireOffsetnull155     fun requireOffset() = anchoredDraggableState.requireOffset()
156 
157     /** Whether the back layer is revealed. */
158     val isRevealed: Boolean
159         get() = anchoredDraggableState.currentValue == Revealed
160 
161     /** Whether the back layer is concealed. */
162     val isConcealed: Boolean
163         get() = anchoredDraggableState.currentValue == Concealed
164 
165     /**
166      * Reveal the back layer with animation and suspend until it if fully revealed or animation has
167      * been cancelled. This method will throw [CancellationException] if the animation is
168      * interrupted
169      */
170     suspend fun reveal() = anchoredDraggableState.animateTo(targetValue = Revealed)
171 
172     /**
173      * Conceal the back layer with animation and suspend until it if fully concealed or animation
174      * has been cancelled. This method will throw [CancellationException] if the animation is
175      * interrupted
176      */
177     suspend fun conceal() = anchoredDraggableState.animateTo(targetValue = Concealed)
178 
179     /**
180      * The fraction of the offset between [from] and [to], as a fraction between [0f..1f], or 1f if
181      * [from] is equal to [to].
182      *
183      * @param from The starting value used to calculate the distance
184      * @param to The end value used to calculate the distance
185      */
186     @FloatRange(from = 0.0, to = 1.0)
187     fun progress(from: BackdropValue, to: BackdropValue): Float {
188         val fromOffset = anchoredDraggableState.anchors.positionOf(from)
189         val toOffset = anchoredDraggableState.anchors.positionOf(to)
190         val currentOffset =
191             anchoredDraggableState.offset.coerceIn(
192                 min(fromOffset, toOffset), // fromOffset might be > toOffset
193                 max(fromOffset, toOffset)
194             )
195         val fraction = (currentOffset - fromOffset) / (toOffset - fromOffset)
196         return if (fraction.isNaN()) 1f else abs(fraction)
197     }
198 
199     internal val anchoredDraggableState =
200         AnchoredDraggableState(
201             initialValue = initialValue,
202             animationSpec = animationSpec,
203             confirmValueChange = confirmValueChange,
<lambda>null204             positionalThreshold = { with(requireDensity()) { PositionalThreshold.toPx() } },
<lambda>null205             velocityThreshold = { with(requireDensity()) { VelocityThreshold.toPx() } }
206         )
207 
208     internal var density: Density? = null
209 
requireDensitynull210     private fun requireDensity() =
211         requireNotNull(density) {
212             "The density on BackdropScaffoldState ($this) was not set." +
213                 " Did you use BackdropScaffoldState with " +
214                 "the BackdropScaffold composable?"
215         }
216 
217     internal val nestedScrollConnection =
218         ConsumeSwipeNestedScrollConnection(anchoredDraggableState, Orientation.Vertical)
219 
220     companion object {
221 
222         /** The default [Saver] implementation for [BackdropScaffoldState]. */
Savernull223         fun Saver(
224             animationSpec: AnimationSpec<Float>,
225             confirmStateChange: (BackdropValue) -> Boolean,
226             snackbarHostState: SnackbarHostState,
227             density: Density
228         ): Saver<BackdropScaffoldState, *> =
229             Saver(
230                 save = { it.anchoredDraggableState.currentValue },
<lambda>null231                 restore = {
232                     BackdropScaffoldState(
233                         initialValue = it,
234                         animationSpec = animationSpec,
235                         confirmValueChange = confirmStateChange,
236                         snackbarHostState = snackbarHostState,
237                         density = density
238                     )
239                 }
240             )
241     }
242 }
243 
244 /**
245  * Create and [remember] a [BackdropScaffoldState].
246  *
247  * @param initialValue The initial value of the state.
248  * @param animationSpec The default animation that will be used to animate to a new state.
249  * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
250  * @param snackbarHostState The [SnackbarHostState] used to show snackbars inside the scaffold.
251  */
252 @Composable
rememberBackdropScaffoldStatenull253 fun rememberBackdropScaffoldState(
254     initialValue: BackdropValue,
255     animationSpec: AnimationSpec<Float> = BackdropScaffoldDefaults.AnimationSpec,
256     confirmStateChange: (BackdropValue) -> Boolean = { true },
<lambda>null257     snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
258 ): BackdropScaffoldState {
259     val density = LocalDensity.current
260     return rememberSaveable(
261         animationSpec,
262         confirmStateChange,
263         snackbarHostState,
264         saver =
265             BackdropScaffoldState.Saver(
266                 animationSpec = animationSpec,
267                 confirmStateChange = confirmStateChange,
268                 snackbarHostState = snackbarHostState,
269                 density = density
270             )
<lambda>null271     ) {
272         BackdropScaffoldState(
273             initialValue = initialValue,
274             animationSpec = animationSpec,
275             confirmValueChange = confirmStateChange,
276             snackbarHostState = snackbarHostState,
277             density = density
278         )
279     }
280 }
281 
282 /**
283  * [Material Design backdrop](https://material.io/components/backdrop)
284  *
285  * A backdrop appears behind all other surfaces in an app, displaying contextual and actionable
286  * content.
287  *
288  * ![Backdrop
289  * image](https://developer.android.com/images/reference/androidx/compose/material/backdrop.png)
290  *
291  * This component provides an API to put together several material components to construct your
292  * screen. For a similar component which implements the basic material design layout strategy with
293  * app bars, floating action buttons and navigation drawers, use the standard [Scaffold]. For
294  * similar component that uses a bottom sheet as the centerpiece of the screen, use
295  * [BottomSheetScaffold].
296  *
297  * Either the back layer or front layer can be active at a time. When the front layer is active, it
298  * sits at an offset below the top of the screen. This is the [peekHeight] and defaults to 56dp
299  * which is the default app bar height. When the front layer is inactive, it sticks to the height of
300  * the back layer's content if [stickyFrontLayer] is set to `true` and the height of the front layer
301  * exceeds the [headerHeight], and otherwise it minimizes to the [headerHeight]. To switch between
302  * the back layer and front layer, you can either swipe on the front layer if [gesturesEnabled] is
303  * set to `true` or use any of the methods in [BackdropScaffoldState].
304  *
305  * The scaffold also contains an app bar, which by default is placed above the back layer's content.
306  * If [persistentAppBar] is set to `false`, then the backdrop will not show the app bar when the
307  * back layer is revealed; instead it will switch between the app bar and the provided content with
308  * an animation. For best results, the [peekHeight] should match the app bar height. To show a
309  * snackbar, use the method `showSnackbar` of [BackdropScaffoldState.snackbarHostState].
310  *
311  * A simple example of a backdrop scaffold looks like this:
312  *
313  * @sample androidx.compose.material.samples.BackdropScaffoldSample
314  * @param appBar App bar for the back layer. Make sure that the [peekHeight] is equal to the height
315  *   of the app bar, so that the app bar is fully visible. Consider using [TopAppBar] but set the
316  *   elevation to 0dp and background color to transparent as a surface is already provided.
317  * @param backLayerContent The content of the back layer.
318  * @param frontLayerContent The content of the front layer.
319  * @param modifier Optional [Modifier] for the root of the scaffold.
320  * @param scaffoldState The state of the scaffold.
321  * @param snackbarHost The component hosting the snackbars shown inside the scaffold.
322  * @param gesturesEnabled Whether or not the backdrop can be interacted with by gestures.
323  * @param peekHeight The height of the visible part of the back layer when it is concealed.
324  * @param headerHeight The minimum height of the front layer when it is inactive.
325  * @param persistentAppBar Whether the app bar should be shown when the back layer is revealed. By
326  *   default, it will always be shown above the back layer's content. If this is set to `false`, the
327  *   back layer will automatically switch between the app bar and its content with an animation.
328  * @param stickyFrontLayer Whether the front layer should stick to the height of the back layer.
329  * @param backLayerBackgroundColor The background color of the back layer.
330  * @param backLayerContentColor The preferred content color provided by the back layer to its
331  *   children. Defaults to the matching content color for [backLayerBackgroundColor], or if that is
332  *   not a color from the theme, this will keep the same content color set above the back layer.
333  * @param frontLayerShape The shape of the front layer.
334  * @param frontLayerElevation The elevation of the front layer.
335  * @param frontLayerBackgroundColor The background color of the front layer.
336  * @param frontLayerContentColor The preferred content color provided by the back front to its
337  *   children. Defaults to the matching content color for [frontLayerBackgroundColor], or if that is
338  *   not a color from the theme, this will keep the same content color set above the front layer.
339  * @param frontLayerScrimColor The color of the scrim applied to the front layer when the back layer
340  *   is revealed. If the color passed is [Color.Unspecified], then a scrim will not be applied and
341  *   interaction with the front layer will not be blocked when the back layer is revealed.
342  */
343 @OptIn(ExperimentalMaterialApi::class)
344 @Composable
BackdropScaffoldnull345 fun BackdropScaffold(
346     appBar: @Composable () -> Unit,
347     backLayerContent: @Composable () -> Unit,
348     frontLayerContent: @Composable () -> Unit,
349     modifier: Modifier = Modifier,
350     scaffoldState: BackdropScaffoldState = rememberBackdropScaffoldState(Concealed),
351     snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
352     gesturesEnabled: Boolean = true,
353     peekHeight: Dp = BackdropScaffoldDefaults.PeekHeight,
354     headerHeight: Dp = BackdropScaffoldDefaults.HeaderHeight,
355     persistentAppBar: Boolean = true,
356     stickyFrontLayer: Boolean = true,
357     backLayerBackgroundColor: Color = MaterialTheme.colors.primary,
358     backLayerContentColor: Color = contentColorFor(backLayerBackgroundColor),
359     frontLayerShape: Shape = BackdropScaffoldDefaults.frontLayerShape,
360     frontLayerElevation: Dp = BackdropScaffoldDefaults.FrontLayerElevation,
361     frontLayerBackgroundColor: Color = MaterialTheme.colors.surface,
362     frontLayerContentColor: Color = contentColorFor(frontLayerBackgroundColor),
363     frontLayerScrimColor: Color = BackdropScaffoldDefaults.frontLayerScrimColor,
364 ) {
365     // b/278692145 Remove this once deprecated methods without density are removed
366     val density = LocalDensity.current
<lambda>null367     SideEffect { scaffoldState.density = density }
368 
<lambda>null369     val peekHeightPx = with(LocalDensity.current) { peekHeight.toPx() }
<lambda>null370     val headerHeightPx = with(LocalDensity.current) { headerHeight.toPx() }
371 
372     val backLayer =
<lambda>null373         @Composable {
374             if (persistentAppBar) {
375                 Column {
376                     appBar()
377                     backLayerContent()
378                 }
379             } else {
380                 BackLayerTransition(
381                     scaffoldState.anchoredDraggableState.targetValue,
382                     appBar,
383                     backLayerContent
384                 )
385             }
386         }
<lambda>null387     val calculateBackLayerConstraints: (Constraints) -> Constraints = {
388         it.copy(minWidth = 0, minHeight = 0).offset(vertical = -headerHeightPx.roundToInt())
389     }
390 
391     val state = scaffoldState.anchoredDraggableState
392 
393     // Back layer
<lambda>null394     Surface(color = backLayerBackgroundColor, contentColor = backLayerContentColor) {
395         val scope = rememberCoroutineScope()
396         BackdropStack(modifier.fillMaxSize(), backLayer, calculateBackLayerConstraints) {
397             constraints,
398             backLayerHeight ->
399             var revealedHeight = constraints.maxHeight - headerHeightPx
400             if (stickyFrontLayer) {
401                 revealedHeight = min(revealedHeight, backLayerHeight)
402             }
403 
404             val nestedScroll =
405                 if (gesturesEnabled) {
406                     Modifier.nestedScroll(scaffoldState.nestedScrollConnection)
407                 } else {
408                     Modifier
409                 }
410 
411             // Front layer
412             Surface(
413                 nestedScroll
414                     .draggableAnchors(state, Orientation.Vertical) { layoutSize, _ ->
415                         val sheetHeight = layoutSize.height.toFloat()
416                         val collapsedHeight = layoutSize.height - peekHeightPx
417                         val newAnchors = DraggableAnchors {
418                             if (sheetHeight == 0f || sheetHeight == peekHeightPx) {
419                                 Concealed at collapsedHeight
420                             } else {
421                                 Concealed at peekHeightPx
422                                 Revealed at revealedHeight
423                             }
424                         }
425                         val newTarget =
426                             when (scaffoldState.targetValue) {
427                                 Concealed -> Concealed
428                                 Revealed ->
429                                     if (newAnchors.hasAnchorFor(Revealed)) Revealed else Concealed
430                             }
431                         return@draggableAnchors newAnchors to newTarget
432                     }
433                     .anchoredDraggable(
434                         state = state,
435                         orientation = Orientation.Vertical,
436                         enabled = gesturesEnabled,
437                     )
438                     .semantics {
439                         if (scaffoldState.isConcealed) {
440                             collapse {
441                                 if (scaffoldState.confirmValueChange(Revealed)) {
442                                     scope.launch { scaffoldState.reveal() }
443                                 }
444                                 true
445                             }
446                         } else {
447                             expand {
448                                 if (scaffoldState.confirmValueChange(Concealed)) {
449                                     scope.launch { scaffoldState.conceal() }
450                                 }
451                                 true
452                             }
453                         }
454                     },
455                 shape = frontLayerShape,
456                 elevation = frontLayerElevation,
457                 color = frontLayerBackgroundColor,
458                 contentColor = frontLayerContentColor
459             ) {
460                 Box(Modifier.padding(bottom = peekHeight)) {
461                     frontLayerContent()
462                     Scrim(
463                         color = frontLayerScrimColor,
464                         onDismiss = {
465                             if (gesturesEnabled && scaffoldState.confirmValueChange(Concealed)) {
466                                 scope.launch { scaffoldState.conceal() }
467                             }
468                         },
469                         visible = scaffoldState.targetValue == Revealed
470                     )
471                 }
472             }
473 
474             // Snackbar host
475             Box(
476                 Modifier.padding(
477                     bottom =
478                         if (
479                             scaffoldState.isRevealed &&
480                                 revealedHeight == constraints.maxHeight - headerHeightPx
481                         )
482                             headerHeight
483                         else 0.dp
484                 ),
485                 contentAlignment = Alignment.BottomCenter
486             ) {
487                 snackbarHost(scaffoldState.snackbarHostState)
488             }
489         }
490     }
491 }
492 
493 @Composable
Scrimnull494 private fun Scrim(color: Color, onDismiss: () -> Unit, visible: Boolean) {
495     if (color.isSpecified) {
496         val alpha by
497             animateFloatAsState(targetValue = if (visible) 1f else 0f, animationSpec = TweenSpec())
498         val dismissModifier =
499             if (visible) {
500                 Modifier.pointerInput(Unit) { detectTapGestures { onDismiss() } }
501             } else {
502                 Modifier
503             }
504         Canvas(Modifier.fillMaxSize().then(dismissModifier)) {
505             drawRect(color = color, alpha = alpha)
506         }
507     }
508 }
509 
510 /**
511  * A shared axis transition, used in the back layer. Both the [appBar] and the [content] shift
512  * vertically, while they crossfade. It is very important that both are composed and measured, even
513  * if invisible, and that this component is as large as both of them.
514  */
515 @Composable
BackLayerTransitionnull516 private fun BackLayerTransition(
517     target: BackdropValue,
518     appBar: @Composable () -> Unit,
519     content: @Composable () -> Unit
520 ) {
521     // The progress of the animation between Revealed (0) and Concealed (2).
522     // The midpoint (1) is the point where the appBar and backContent are switched.
523     val animationProgress by
524         animateFloatAsState(
525             targetValue = if (target == Revealed) 0f else 2f,
526             animationSpec = TweenSpec()
527         )
528     val animationSlideOffset = with(LocalDensity.current) { AnimationSlideOffset.toPx() }
529 
530     Box {
531         Box(
532             Modifier.layout { measurable, constraints ->
533                     val appBarFloat = (animationProgress - 1).fastCoerceIn(0f, 1f)
534                     val placeable = measurable.measure(constraints)
535                     layout(placeable.width, placeable.height) {
536                         placeable.place(0, 0, zIndex = appBarFloat)
537                     }
538                 }
539                 .graphicsLayer {
540                     val appBarFloat = (animationProgress - 1).fastCoerceIn(0f, 1f)
541                     alpha = appBarFloat
542                     translationY = (1 - appBarFloat) * animationSlideOffset
543                 }
544         ) {
545             appBar()
546         }
547         Box(
548             @Suppress("SuspiciousIndentation") // b/320904953
549             Modifier.layout { measurable, constraints ->
550                     val contentFloat = (1 - animationProgress).fastCoerceIn(0f, 1f)
551                     val placeable = measurable.measure(constraints)
552                     layout(placeable.width, placeable.height) {
553                         placeable.place(0, 0, zIndex = contentFloat)
554                     }
555                 }
556                 .graphicsLayer {
557                     val contentFloat = (1 - animationProgress).fastCoerceIn(0f, 1f)
558                     alpha = contentFloat
559                     translationY = (1 - contentFloat) * animationSlideOffset
560                 }
561         ) {
562             content()
563         }
564     }
565 }
566 
567 @Composable
BackdropStacknull568 private fun BackdropStack(
569     modifier: Modifier,
570     backLayer: @Composable () -> Unit,
571     calculateBackLayerConstraints: (Constraints) -> Constraints,
572     frontLayer: @Composable (Constraints, Float) -> Unit
573 ) {
574     SubcomposeLayout(modifier) { constraints ->
575         val backLayerPlaceable =
576             subcompose(BackdropLayers.Back, backLayer)
577                 .first()
578                 .measure(calculateBackLayerConstraints(constraints))
579 
580         val backLayerHeight = backLayerPlaceable.height.toFloat()
581 
582         val placeables =
583             subcompose(BackdropLayers.Front) { frontLayer(constraints, backLayerHeight) }
584                 .fastMap { it.measure(constraints) }
585 
586         var maxWidth = max(constraints.minWidth, backLayerPlaceable.width)
587         var maxHeight = max(constraints.minHeight, backLayerPlaceable.height)
588         placeables.fastForEach {
589             maxWidth = max(maxWidth, it.width)
590             maxHeight = max(maxHeight, it.height)
591         }
592 
593         layout(maxWidth, maxHeight) {
594             backLayerPlaceable.placeRelative(0, 0)
595             placeables.fastForEach { it.placeRelative(0, 0) }
596         }
597     }
598 }
599 
600 private enum class BackdropLayers {
601     Back,
602     Front
603 }
604 
605 /** Contains useful defaults for [BackdropScaffold]. */
606 object BackdropScaffoldDefaults {
607 
608     /** The default peek height of the back layer. */
609     val PeekHeight = 56.dp
610 
611     /** The default header height of the front layer. */
612     val HeaderHeight = 48.dp
613 
614     /** The default shape of the front layer. */
615     val frontLayerShape: Shape
616         @Composable
617         get() =
618             MaterialTheme.shapes.large.copy(
619                 topStart = CornerSize(16.dp),
620                 topEnd = CornerSize(16.dp)
621             )
622 
623     /** The default elevation of the front layer. */
624     val FrontLayerElevation = 1.dp
625 
626     /** The default color of the scrim applied to the front layer. */
627     val frontLayerScrimColor: Color
628         @Composable get() = MaterialTheme.colors.surface.copy(alpha = 0.60f)
629 
630     /** The default animation spec used by [BottomSheetScaffoldState]. */
631     val AnimationSpec: AnimationSpec<Float> =
632         tween(durationMillis = 300, easing = FastOutSlowInEasing)
633 }
634 
635 private val AnimationSlideOffset = 20.dp
636 private val VelocityThreshold = 125.dp
637 private val PositionalThreshold = 56.dp
638 
639 @OptIn(ExperimentalMaterialApi::class)
ConsumeSwipeNestedScrollConnectionnull640 internal fun ConsumeSwipeNestedScrollConnection(
641     state: AnchoredDraggableState<*>,
642     orientation: Orientation
643 ): NestedScrollConnection =
644     object : NestedScrollConnection {
645         override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
646             val delta = available.toFloat()
647             return if (delta < 0 && source == NestedScrollSource.UserInput) {
648                 state.dispatchRawDelta(delta).toOffset()
649             } else {
650                 Offset.Zero
651             }
652         }
653 
654         override fun onPostScroll(
655             consumed: Offset,
656             available: Offset,
657             source: NestedScrollSource
658         ): Offset {
659             return if (source == NestedScrollSource.UserInput) {
660                 state.dispatchRawDelta(available.toFloat()).toOffset()
661             } else {
662                 Offset.Zero
663             }
664         }
665 
666         override suspend fun onPreFling(available: Velocity): Velocity {
667             val toFling = available.toFloat()
668             val currentOffset = state.requireOffset()
669             return if (toFling < 0 && currentOffset > state.anchors.minAnchor()) {
670                 state.settle(velocity = toFling)
671                 // since we go to the anchor with tween settling, consume all for the best UX
672                 available
673             } else {
674                 Velocity.Zero
675             }
676         }
677 
678         override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
679             state.settle(velocity = available.toFloat())
680             return available
681         }
682 
683         private fun Float.toOffset(): Offset =
684             Offset(
685                 x = if (orientation == Orientation.Horizontal) this else 0f,
686                 y = if (orientation == Orientation.Vertical) this else 0f
687             )
688 
689         @JvmName("velocityToFloat")
690         private fun Velocity.toFloat() = if (orientation == Orientation.Horizontal) x else y
691 
692         @JvmName("offsetToFloat")
693         private fun Offset.toFloat(): Float = if (orientation == Orientation.Horizontal) x else y
694     }
695