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