From 3e58192bc586b99cbeba8e7e8c98d30dff2e9170 Mon Sep 17 00:00:00 2001 From: Alexander Karkossa Date: Mon, 6 May 2024 22:55:27 +0200 Subject: [PATCH] fix: switch internal to BottomSheetScaffold --- .idea/inspectionProfiles/Project_Default.xml | 41 +++ bottom-drawer-scaffold/build.gradle | 2 +- .../AnchoredDraggableStateExt.kt | 67 ---- .../BottomDrawerScaffold.kt | 304 ++++++------------ .../sample/MainActivity.kt | 10 +- 5 files changed, 141 insertions(+), 283 deletions(-) create mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 bottom-drawer-scaffold/src/main/java/de/charlex/compose/bottomdrawerscaffold/AnchoredDraggableStateExt.kt diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..44ca2d9 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,41 @@ + + + + \ No newline at end of file diff --git a/bottom-drawer-scaffold/build.gradle b/bottom-drawer-scaffold/build.gradle index f2cd22f..2eca6f8 100644 --- a/bottom-drawer-scaffold/build.gradle +++ b/bottom-drawer-scaffold/build.gradle @@ -8,7 +8,7 @@ apply from: '../buildCompose.gradle' ext { PUBLISH_GROUP_ID = 'de.charlex.compose' - PUBLISH_VERSION = '2.0.0-beta02' + PUBLISH_VERSION = '2.0.0-beta03' PUBLISH_ARTIFACT_ID = 'bottom-drawer-scaffold' } diff --git a/bottom-drawer-scaffold/src/main/java/de/charlex/compose/bottomdrawerscaffold/AnchoredDraggableStateExt.kt b/bottom-drawer-scaffold/src/main/java/de/charlex/compose/bottomdrawerscaffold/AnchoredDraggableStateExt.kt deleted file mode 100644 index 9346191..0000000 --- a/bottom-drawer-scaffold/src/main/java/de/charlex/compose/bottomdrawerscaffold/AnchoredDraggableStateExt.kt +++ /dev/null @@ -1,67 +0,0 @@ -package de.charlex.compose.bottomdrawerscaffold - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.gestures.AnchoredDraggableState -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.unit.Velocity - -@OptIn(ExperimentalFoundationApi::class) -internal fun AnchoredDraggableState.createPreUpPostDownNestedScrollConnection( - orientation: Orientation, - onFling: (velocity: Float) -> Unit -): NestedScrollConnection { - return object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - val delta = available.toFloat() - return if (delta < 0 && source == NestedScrollSource.Drag) { - dispatchRawDelta(delta).toOffset() - } else { - Offset.Zero - } - } - - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource - ): Offset { - return if (source == NestedScrollSource.Drag) { - dispatchRawDelta(available.toFloat()).toOffset() - } else { - Offset.Zero - } - } - - override suspend fun onPreFling(available: Velocity): Velocity { - val toFling = available.toFloat() - val currentOffset = requireOffset() - val minAnchor = anchors.minAnchor() - return if (toFling < 0 && currentOffset > minAnchor) { - onFling(toFling) - // since we go to the anchor with tween settling, consume all for the best UX - available - } else { - Velocity.Zero - } - } - - override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - onFling(available.toFloat()) - return available - } - - private fun Float.toOffset(): Offset = Offset( - x = if (orientation == Orientation.Horizontal) this else 0f, - y = if (orientation == Orientation.Vertical) this else 0f - ) - - @JvmName("velocityToFloat") - private fun Velocity.toFloat() = if (orientation == Orientation.Horizontal) x else y - - @JvmName("offsetToFloat") - private fun Offset.toFloat(): Float = if (orientation == Orientation.Horizontal) x else y - } -} diff --git a/bottom-drawer-scaffold/src/main/java/de/charlex/compose/bottomdrawerscaffold/BottomDrawerScaffold.kt b/bottom-drawer-scaffold/src/main/java/de/charlex/compose/bottomdrawerscaffold/BottomDrawerScaffold.kt index 248d47a..a82a8e7 100644 --- a/bottom-drawer-scaffold/src/main/java/de/charlex/compose/bottomdrawerscaffold/BottomDrawerScaffold.kt +++ b/bottom-drawer-scaffold/src/main/java/de/charlex/compose/bottomdrawerscaffold/BottomDrawerScaffold.kt @@ -1,13 +1,5 @@ package de.charlex.compose.bottomdrawerscaffold -import androidx.compose.animation.core.AnimationSpec -import androidx.compose.animation.core.tween -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.gestures.AnchoredDraggableState -import androidx.compose.foundation.gestures.DraggableAnchors -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.anchoredDraggable -import androidx.compose.foundation.gestures.animateTo import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column @@ -23,73 +15,42 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeightIn import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.BottomSheetScaffold +import androidx.compose.material3.BottomSheetScaffoldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FabPosition import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SheetValue +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.contentColorFor +import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.semantics.collapse -import androidx.compose.ui.semantics.expand -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch -import kotlin.math.roundToInt -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun rememberBottomDrawerScaffoldState( - initialValue: BottomDrawerValue = BottomDrawerValue.Collapsed, - positionalThreshold: ((totalDistance: Float) -> Float)? = null, - velocityThreshold: (() -> Float)? = null, - animationSpec: AnimationSpec = tween(), - confirmValueChange: (newValue: BottomDrawerValue) -> Boolean = { true } -): AnchoredDraggableState { - val density = LocalDensity.current - val maxHeight = with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() } - return remember { - AnchoredDraggableState( - initialValue = initialValue, - positionalThreshold = positionalThreshold ?: { with(density) { 56.dp.toPx() } }, - velocityThreshold = velocityThreshold ?: { with(density) { 125.dp.toPx() } }, - animationSpec = animationSpec, - anchors = DraggableAnchors { - BottomDrawerValue.Collapsed at maxHeight - BottomDrawerValue.Expanded at 0f - }, - confirmValueChange = confirmValueChange - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) @Composable @ExperimentalMaterial3Api fun BottomDrawerScaffold( modifier: Modifier = Modifier, - bottomDrawerScaffoldState: AnchoredDraggableState = rememberBottomDrawerScaffoldState(), + bottomSheetScaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(), topBar: @Composable (() -> Unit)? = null, bottomBar: @Composable (() -> Unit)? = null, gesturesEnabled: Boolean = true, drawerModifier: Modifier = Modifier, - snackbarHost: @Composable () -> Unit = {}, + snackbarHost: @Composable (SnackbarHostState) -> Unit = {}, floatingActionButton: @Composable (() -> Unit)? = null, floatingActionButtonPosition: FabPosition = FabPosition.End, drawerGesturesEnabled: Boolean? = null, @@ -106,10 +67,6 @@ fun BottomDrawerScaffold( contentColor: Color = MaterialTheme.colorScheme.contentColorFor(backgroundColor), content: @Composable (PaddingValues) -> Unit ) { - val scope = rememberCoroutineScope() - - val orientation = Orientation.Vertical - Scaffold( modifier = modifier, contentWindowInsets = WindowInsets.navigationBars.exclude(WindowInsets.statusBars), @@ -122,192 +79,119 @@ fun BottomDrawerScaffold( floatingActionButton = { floatingActionButton?.invoke() }, - snackbarHost = snackbarHost, floatingActionButtonPosition = floatingActionButtonPosition, ) { scaffoldPaddingValues -> + val scope = rememberCoroutineScope() + BoxWithConstraints( - modifier = Modifier.padding(scaffoldPaddingValues), - contentAlignment = Alignment.BottomCenter + modifier = Modifier + .padding(scaffoldPaddingValues) ) { val fullHeight = constraints.maxHeight.toFloat() val peekHeightPx = with(LocalDensity.current) { drawerPeekHeight.toPx() } var bottomDrawerHeight by remember { mutableStateOf(fullHeight) } - val density = LocalDensity.current - val topPadding = if (topBar == null) { - with(density) { WindowInsets.statusBars.asPaddingValues(density).calculateTopPadding().toPx() } - } else { - 0f - } - - LaunchedEffect(fullHeight, peekHeightPx, bottomDrawerHeight) { - bottomDrawerScaffoldState.updateAnchors( - newAnchors = DraggableAnchors { - BottomDrawerValue.Collapsed at (fullHeight - peekHeightPx) - BottomDrawerValue.Expanded at topPadding + BottomSheetScaffold( + modifier = Modifier, + scaffoldState = bottomSheetScaffoldState, + sheetDragHandle = null, + sheetSwipeEnabled = drawerGesturesEnabled ?: gesturesEnabled, + sheetContainerColor = Color.Transparent, + sheetPeekHeight = drawerPeekHeight + 30.dp, + sheetTonalElevation = 0.dp, + sheetShadowElevation = 0.dp, + snackbarHost = snackbarHost, + sheetContent = { + val topPadding = if (topBar == null) { + WindowInsets.statusBars.asPaddingValues(LocalDensity.current).calculateTopPadding() + } else { + 0.dp } - ) - } - - val anchoredDraggableModifier = Modifier - .nestedScroll( - bottomDrawerScaffoldState.createPreUpPostDownNestedScrollConnection( - orientation = orientation, - onFling = { scope.launch { bottomDrawerScaffoldState.settle(it) } } - ) - ) - .anchoredDraggable( - state = bottomDrawerScaffoldState, - orientation = orientation, - enabled = drawerGesturesEnabled ?: gesturesEnabled, - ) - .semantics { - if (peekHeightPx != bottomDrawerHeight) { - if (bottomDrawerScaffoldState.isCollapsed()) { - expand { - scope.launch { bottomDrawerScaffoldState.expand() } - true - } - } else { - collapse { - scope.launch { bottomDrawerScaffoldState.collapse() } - true + Surface( + Modifier + .fillMaxWidth() + .requiredHeightIn( + min = drawerPeekHeight + ) + .padding( + top = drawerPadding + topPadding, + start = drawerPadding, + end = drawerPadding, + ) + .onGloballyPositioned { + bottomDrawerHeight = it.size.height.toFloat() } + .then( + drawerModifier + ), + shape = drawerShape, + shadowElevation = drawerShadowElevation, + tonalElevation = drawerTonalElevation, + color = drawerBackgroundColor, + contentColor = drawerContentColor, + content = { + Column( + content = { + drawerContent() + } + ) } - } + ) } - - val child = @Composable { - BottomDrawerScaffoldStack( - body = { - Surface( - color = backgroundColor, - contentColor = contentColor - ) { - Box(Modifier.fillMaxSize()) { - content(PaddingValues(bottom = drawerPeekHeight)) - - Scrim( - open = bottomDrawerScaffoldState.isExpanded(), - onClose = { - if (gesturesEnabled) { - scope.launch { bottomDrawerScaffoldState.collapse() } - } - }, - fraction = { - calculateFraction(fullHeight - peekHeightPx, fullHeight - bottomDrawerHeight, bottomDrawerScaffoldState.requireOffset()) - }, - color = drawerScrimColor - ) - } - } - }, - bottomDrawer = { - val density = LocalDensity.current - Surface( - anchoredDraggableModifier - .fillMaxWidth() - .requiredHeightIn(min = drawerPeekHeight, max = with(density) { bottomDrawerHeight.toDp() }) - .onGloballyPositioned { - bottomDrawerHeight = it.size.height.toFloat() + ) { + Surface( + color = backgroundColor, + contentColor = contentColor + ) { + Box(Modifier.fillMaxSize()) { + content(PaddingValues(bottom = drawerPeekHeight)) + + Scrim( + open = bottomSheetScaffoldState.isExpanded(), + onClose = { + if (gesturesEnabled) { + scope.launch { bottomSheetScaffoldState.collapse() } } - .padding( - top = drawerPadding, - start = drawerPadding, - end = drawerPadding, - bottom = if (topBar == null) WindowInsets.statusBars - .asPaddingValues() - .calculateTopPadding() else 0.dp - ) - .then( - drawerModifier - ), - shape = drawerShape, - shadowElevation = drawerShadowElevation, - tonalElevation = drawerTonalElevation, - color = drawerBackgroundColor, - contentColor = drawerContentColor, - content = { - Column( - content = { - drawerContent() - } - ) - } + }, + fraction = { + calculateFraction(fullHeight - peekHeightPx, fullHeight - bottomDrawerHeight, bottomSheetScaffoldState.bottomSheetState.requireOffset()) + }, + color = drawerScrimColor ) - }, - anchoredDraggableState = bottomDrawerScaffoldState, - ) + } + } } - - child() } } } -@OptIn(ExperimentalFoundationApi::class) -@Composable -internal fun BottomDrawerScaffoldStack( - body: @Composable () -> Unit, - bottomDrawer: @Composable () -> Unit, - anchoredDraggableState: AnchoredDraggableState -) { - Layout( - content = { - body() - bottomDrawer() - } - ) { measurables, constraints -> - val placeable = measurables.first().measure(constraints) - - layout(placeable.width, placeable.height) { - placeable.placeRelative(0, 0) - - val (drawerPlaceable) = - measurables.drop(1).map { - it.measure(constraints.copy(minWidth = 0, minHeight = 0)) - } +@OptIn(ExperimentalMaterial3Api::class) +fun BottomSheetScaffoldState.isCollapsed(): Boolean { + return bottomSheetState.hasPartiallyExpandedState +} - val drawerOffsetY = anchoredDraggableState.requireOffset().roundToInt() +@OptIn(ExperimentalMaterial3Api::class) +fun BottomSheetScaffoldState.isExpanded(): Boolean { + return bottomSheetState.hasExpandedState +} - drawerPlaceable.placeRelative(0, drawerOffsetY) - } +@OptIn(ExperimentalMaterial3Api::class) +suspend fun BottomSheetScaffoldState.toggle() { + if (bottomSheetState.targetValue == SheetValue.Expanded) { + bottomSheetState.partialExpand() + } else if (bottomSheetState.targetValue == SheetValue.PartiallyExpanded) { + bottomSheetState.expand() } } -enum class BottomDrawerValue { - /** - * The bottom drawer is visible, but only showing its peek height. - */ - Collapsed, - - /** - * The bottom drawer is visible at its maximum height. - */ - Expanded +@OptIn(ExperimentalMaterial3Api::class) +suspend fun BottomSheetScaffoldState.collapse() { + bottomSheetState.partialExpand() } -@OptIn(ExperimentalFoundationApi::class) -fun AnchoredDraggableState.isExpanded(): Boolean = - currentValue == BottomDrawerValue.Expanded - -@OptIn(ExperimentalFoundationApi::class) -fun AnchoredDraggableState.isCollapsed(): Boolean = - currentValue == BottomDrawerValue.Collapsed - -@OptIn(ExperimentalFoundationApi::class) -suspend fun AnchoredDraggableState.expand() = animateTo(BottomDrawerValue.Expanded) - -@OptIn(ExperimentalFoundationApi::class) -suspend fun AnchoredDraggableState.collapse() = animateTo(BottomDrawerValue.Collapsed) - -@OptIn(ExperimentalFoundationApi::class) -suspend fun AnchoredDraggableState.toggle() { - if (targetValue == BottomDrawerValue.Collapsed) { - expand() - } else { - collapse() - } +@OptIn(ExperimentalMaterial3Api::class) +suspend fun BottomSheetScaffoldState.expand() { + bottomSheetState.expand() } internal fun calculateFraction(a: Float, b: Float, pos: Float): Float { diff --git a/example/src/main/java/de/charlex/compose/bottomdrawerscaffold/sample/MainActivity.kt b/example/src/main/java/de/charlex/compose/bottomdrawerscaffold/sample/MainActivity.kt index 50be99f..db22bb4 100644 --- a/example/src/main/java/de/charlex/compose/bottomdrawerscaffold/sample/MainActivity.kt +++ b/example/src/main/java/de/charlex/compose/bottomdrawerscaffold/sample/MainActivity.kt @@ -46,6 +46,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -58,7 +59,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import de.charlex.compose.bottomdrawerscaffold.BottomDrawerScaffold import de.charlex.compose.bottomdrawerscaffold.isCollapsed -import de.charlex.compose.bottomdrawerscaffold.rememberBottomDrawerScaffoldState import de.charlex.compose.bottomdrawerscaffold.toggle import kotlinx.coroutines.launch @@ -84,11 +84,11 @@ fun Content() { val coroutineScope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } - val bottomDrawerScaffoldState = rememberBottomDrawerScaffoldState() + val bottomSheetScaffoldState = rememberBottomSheetScaffoldState() BottomDrawerScaffold( modifier = Modifier, - bottomDrawerScaffoldState = bottomDrawerScaffoldState, + bottomSheetScaffoldState = bottomSheetScaffoldState, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, @@ -116,7 +116,7 @@ fun Content() { actions = { IconButton(onClick = { coroutineScope.launch { - bottomDrawerScaffoldState.toggle() + bottomSheetScaffoldState.toggle() } }) { Icon(Icons.Filled.Menu, "Menu icon") @@ -130,7 +130,7 @@ fun Content() { drawerPadding = 10.dp, drawerContent = { val lazyListState = rememberLazyListState() - val collapsed = bottomDrawerScaffoldState.isCollapsed() + val collapsed = bottomSheetScaffoldState.isCollapsed() LaunchedEffect(collapsed) { if (collapsed) {