How to set half expanded height for bottomsheet using BottomSheetScaffold in compose?
Asked Answered
H

4

6

I need to show my bottomsheet first in collapsed state. And on swipping bottomsheet up, it should first fix in half height of screen first. Again on swipping up , it should expand to max height of screen. Same during collapsing. First from max height to half height, then to peek height(the height of bottomsheet which will be visible in collapsed state). Is there any way to achieve it using BottomSheetScaffold?

Hasa answered 12/10, 2022 at 11:56 Comment(1)
let me guess, if I understand it correctly, you want this to be implemented as a drag/swipe gesture right?, because this can be done just by using a button or some trigger outside, so I'm curious if those half~quarter close/open states should happen during swiping/draggingDaguerreotype
Z
12

I started write that solution for you. You can beautify it

enum class ExpandedType {
    HALF, FULL, COLLAPSED
}

  @Composable
private fun BottomSheet() {
    val configuration = LocalConfiguration.current
    val screenHeight = configuration.screenHeightDp
    var expandedType by remember {
        mutableStateOf(ExpandedType.COLLAPSED)
    }
    val height by animateIntAsState(
        when (expandedType) {
            ExpandedType.HALF -> screenHeight / 2
            ExpandedType.FULL -> screenHeight
            ExpandedType.COLLAPSED -> 70
        }
    )
    val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
        bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
    )
    BottomSheetScaffold(
        scaffoldState = bottomSheetScaffoldState,
        sheetElevation = 8.dp,
        sheetShape = RoundedCornerShape(
            bottomStart = 0.dp,
            bottomEnd = 0.dp,
            topStart = 12.dp,
            topEnd = 12.dp
        ),
        sheetContent = {
            var isUpdated = false
            Box(
                Modifier
                    .fillMaxWidth()
                    .height(height.dp)
                    .pointerInput(Unit) {
                        detectVerticalDragGestures(
                            onVerticalDrag = { change, dragAmount ->
                                change.consume()
                                if (!isUpdated) {
                                    expandedType = when {
                                        dragAmount < 0 && expandedType == ExpandedType.COLLAPSED -> {
                                            ExpandedType.HALF
                                        }
                                        dragAmount < 0 && expandedType == ExpandedType.HALF -> {
                                            ExpandedType.FULL
                                        }
                                        dragAmount > 0 && expandedType == ExpandedType.FULL -> {
                                            ExpandedType.HALF
                                        }
                                        dragAmount > 0 && expandedType == ExpandedType.HALF -> {
                                            ExpandedType.COLLAPSED
                                        }
                                        else -> {
                                            ExpandedType.FULL
                                        }
                                    }
                                    isUpdated = true
                                }
                            },
                            onDragEnd = {
                                isUpdated = false
                            }
                        )
                    }
                    .background(Color.Red)
            )
        },
        sheetPeekHeight = height.dp
    ) {
        Box(
            Modifier
                .fillMaxSize()
                .background(Color.Black)
        )
    }
}
Zaidazailer answered 12/10, 2022 at 13:37 Comment(5)
Thanks. This was helpful. After customising , i am facing an issue. While collapsing my bottomsheet, its bottom end moving up . What will be the issue?Hasa
Simple fix is set .fillMaxSize() instead of .fillMaxWidth() above .pointerInput(Unit)Burgas
BottomSheetState(BottomSheetValue.Collapsed) its showing unresolved as BottomSheetState can you please tell why is that so?Bashful
@Vera Iureva I like ur approach, However, noticed that detectVerticalDragGestures is not triggered when content() having a lazy column with item with max-width I believe its the expectation since lazy column handles its own gesture mechanism, ex -: internal scrolling I there a way to fix it?Spawn
You can allow to show the dragging handler and placed there the dragging functionality. This is how I got it to work. To be precise I also made sure to get the height of the dragging handler so I could match when collapsing and fully expanding.Illsorted
B
3

Found the way to fix issue about

While collapsing my bottomsheet, its bottom end moving up

Based on accepted answer made some changes, and it seems to solve an issue

        val bottomSheetSt = rememberStandardBottomSheetState(
            skipHiddenState = true,
            initialValue = SheetValue.PartiallyExpanded
        )
        val scaffoldState = rememberBottomSheetScaffoldState(bottomSheetSt)
        var peekHeight: Int by remember { mutableStateOf(0) }


        BottomSheetScaffold(
                scaffoldState = scaffoldState,
                sheetContent = {
                    val scope = rememberCoroutineScope()
                    BottomSheetGestureWrapper(
                        onExpandTypeChanged = {
                            scope.launch {
                                peekHeight = when (it) {
                                    ExpandedType.COLLAPSED -> 70
                                    ExpandedType.FULL -> screenHeight - 46
                                    ExpandedType.HALF -> screenHeight / 2
                                }
                                bottomSheetSt.partialExpand() // Smooth animation to desired height
                            }
                        }
                    ) {
                        // Bottom Sheet content
                    }
                },
                sheetPeekHeight = peekHeight.dp, // <------- Important 
                modifier = Modifier.fillMaxSize(),
                sheetShadowElevation = 0.dp,
                sheetContainerColor = Color.Transparent,
                sheetContentColor = Color.Transparent,
                sheetDragHandle = null,
            ) {
                // Scaffold content
            }

And also moved draggable staff to separate file

@Composable
fun BottomSheetGestureWrapper(
    modifier: Modifier = Modifier,
    onExpandTypeChanged: (ExpandedType) -> Unit,
    content: @Composable () -> Unit
) {

    var expandedType by remember {
        mutableStateOf(ExpandedType.COLLAPSED)
    }

    var isUpdated = false

    LaunchedEffect(key1 = expandedType) {
        onExpandTypeChanged(expandedType)
    }

    Box(
        modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                detectVerticalDragGestures(
                    onVerticalDrag = { change, dragAmount ->
                        change.consume()
                        if (!isUpdated) {
                            expandedType = when {
                                dragAmount < 0 && expandedType == ExpandedType.COLLAPSED -> {
                                    ExpandedType.HALF
                                }

                                dragAmount < 0 && expandedType == ExpandedType.HALF -> {
                                    ExpandedType.FULL
                                }

                                dragAmount > 0 && expandedType == ExpandedType.FULL -> {
                                    ExpandedType.HALF
                                }

                                dragAmount > 0 && expandedType == ExpandedType.HALF -> {
                                    ExpandedType.COLLAPSED
                                }

                                else -> {
                                    expandedType
                                }
                            }
                            isUpdated = true
                        }
                    },
                    onDragEnd = {
                        isUpdated = false
                    }
                )
            }
            .background(Color.White)
    ) {
        content()
    }
}
Burgas answered 21/11, 2023 at 16:7 Comment(3)
screenHeight what is this? can you provide this also?Bashful
@TanishqChawda check first answer. val screenHeight = LocalConfiguration.current.screenHeightDpPedagogics
@Burgas I like ur approach, However, noticed that detectVerticalDragGestures is not triggered when content() having a lazy column with item with max-width I believe its the expectation since lazy column handles its own gesture mechanism, ex -: internal scrolling I there a way to fix it?Spawn
D
0

I wasted lots of time and it works perfectly, with smooth and natural finger animation!

@Composable
fun BottomSheetContent() {
    Surface(
        modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
    ) {
        val density = LocalDensity.current
        val configuration = LocalConfiguration.current
        val initialScreenHeightInDp = configuration.screenHeightDp
        val coroutineScope = rememberCoroutineScope()
        var job by remember { mutableStateOf<Job?>(null) }

        val sheetState = rememberStandardBottomSheetState(SheetValue.PartiallyExpanded)
        val scaffoldState = rememberBottomSheetScaffoldState(sheetState)
        val scope = rememberCoroutineScope()

        var screenSizeInDp by remember { mutableStateOf(DpSize.Zero) }
        val minHeightInDp = 35f.dp

        val centerHeightInDp = (initialScreenHeightInDp.dp - minHeightInDp) / 2
        var currentHeightInDp by remember { mutableStateOf(centerHeightInDp) }

        BottomSheetScaffold(
            scaffoldState = scaffoldState, sheetContent = {
                Box(modifier = Modifier
                    .fillMaxWidth()
                    .height(currentHeightInDp)
                    .background(color = Color.White)
                    .pointerInput(Unit) {
                        detectVerticalDragGestures(onDragStart = {
                            android.util.Log.d("###", "onDragStart")
                        }, onVerticalDrag = { change, dragAmount ->
                            change.consume()
                            currentHeightInDp = if (currentHeightInDp - dragAmount.toDp() < minHeightInDp) {
                                minHeightInDp
                            } else if(currentHeightInDp - dragAmount.toDp() > screenSizeInDp.height) {
                                screenSizeInDp.height
                            } else {
                                currentHeightInDp - dragAmount.toDp()
                            }
                        }, onDragEnd = {
                            job?.cancel()
                            job = coroutineScope.launch {
                                when(currentHeightInDp) {
                                    in minHeightInDp..centerHeightInDp / 2 -> {
                                        while (currentHeightInDp > minHeightInDp) {
                                            currentHeightInDp -= 5.dp
                                            delay(1)
                                        }
                                        currentHeightInDp = minHeightInDp
                                    }
                                    in centerHeightInDp / 2..centerHeightInDp -> {
                                        while (currentHeightInDp < centerHeightInDp) {
                                            currentHeightInDp += 5.dp
                                            delay(1)
                                        }
                                        currentHeightInDp = centerHeightInDp
                                    }
                                    in centerHeightInDp..centerHeightInDp + centerHeightInDp / 2 -> {
                                        while (currentHeightInDp > centerHeightInDp) {
                                            currentHeightInDp -= 5.dp
                                            delay(1)
                                        }
                                        currentHeightInDp = centerHeightInDp
                                    }
                                    in centerHeightInDp + centerHeightInDp / 2..screenSizeInDp.height -> {
                                        while (currentHeightInDp < screenSizeInDp.height) {
                                            currentHeightInDp += 5.dp
                                            delay(1)
                                        }
                                        currentHeightInDp = screenSizeInDp.height
                                    }
                                }
                                job?.cancel()
                                return@launch
                            }

                        })
                    }) {
                    Column(
                        horizontalAlignment = Alignment.CenterHorizontally,
                        modifier = Modifier.fillMaxSize()
                    ) {

                    }
                }
            },
            sheetShape = RoundedCornerShape(
                bottomStart = 0.dp, bottomEnd = 0.dp, topStart = 16.dp, topEnd = 16.dp
            ),
            sheetDragHandle = {},
            modifier = Modifier,
            sheetPeekHeight = currentHeightInDp
        ) {
            Box(
                modifier = Modifier.fillMaxSize().background(Color(0xFFEEEEEE)).onSizeChanged {
                    screenSizeInDp = density.run {
                        DpSize(
                            it.width.toDp(),
                            it.height.toDp()
                        )
                    }
                }, contentAlignment = Alignment.Center
            ) {

            }
        }
    }
}

enter image description here

Dorree answered 11/9 at 20:13 Comment(0)
I
0

I realized the first answer has a good way to handle dragging actions. I want to have the dragging handler visible. So in this demo I applied Material3. I also remain using a custom dragging handler similar to the original one. If I stick to the original handler only the dash area is available to drag.

I also noticed the best match of the full height is to go by the container's height as well.

One more thing, this demo measures the dragging handler height, so the bottom sheet is more accurate how low to go and also half way.

P.S. I would like to test the third answer related to finger dragging.. :)

package info.juanmendez.view.components.generic

import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectVerticalDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SheetValue
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

enum class ExpandedType {
    COLLAPSED, HALF_EXPANDED, EXPANDED
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CustomBottomSheet(
    modifier: Modifier,
    screenHeight: Dp,
) {
    val localDensity = LocalDensity.current
    var expandedType by remember {
        mutableStateOf(ExpandedType.COLLAPSED)
    }

    var dragHandleHeight by remember {
        mutableStateOf(0.dp)
    }

    val sheetHeight by animateDpAsState(
        when (expandedType) {
            ExpandedType.HALF_EXPANDED -> (screenHeight / 2)
            ExpandedType.EXPANDED -> screenHeight
            ExpandedType.COLLAPSED -> dragHandleHeight
        },
        label = "sheetHeight"
    )


    BottomSheetScaffold(
        modifier = Modifier,
        sheetDragHandle = {
            var isUpdated by remember {
                mutableStateOf(false)
            }
            CustomSheetDragHandle(
                modifier = Modifier
                    .onSizeChanged {
                        dragHandleHeight = Dp(it.height / localDensity.density)
                    }
                    .pointerInput(Unit) {
                        detectVerticalDragGestures(
                            onDragEnd = {
                                isUpdated = false
                            }
                        ) { change, dragAmount ->
                            change.consume()
                            if (!isUpdated) {
                                expandedType = when {
                                    dragAmount < 0 -> {
                                        when (expandedType) {
                                            ExpandedType.COLLAPSED -> ExpandedType.HALF_EXPANDED
                                            ExpandedType.HALF_EXPANDED -> ExpandedType.EXPANDED
                                            else -> ExpandedType.COLLAPSED
                                        }
                                    }

                                    dragAmount > 0 -> {
                                        when (expandedType) {
                                            ExpandedType.EXPANDED -> ExpandedType.HALF_EXPANDED
                                            ExpandedType.HALF_EXPANDED -> ExpandedType.COLLAPSED
                                            else -> ExpandedType.COLLAPSED
                                        }
                                    }

                                    else -> ExpandedType.COLLAPSED
                                }
                                isUpdated = true
                            }
                        }
                    },
            )
        },
        sheetContent = {
            Box(
                modifier
                    .fillMaxWidth()
                    .height(sheetHeight - dragHandleHeight)
                    .background(Color.Red)
            )
        },
        sheetPeekHeight = sheetHeight
    ) {

    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomSheetDragHandle(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier
            .fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        BottomSheetDefaults.DragHandle()
    }

}

// PREVIEW
@Preview
@Composable
fun CustomBottomSheetPreview() {
    val localDensity = LocalDensity.current

    var screenHeight by remember {
        mutableStateOf(0.dp)
    }

    MaterialTheme {
        Column(
            modifier = Modifier
                .background(Color.Blue)
                .fillMaxSize()
                .onSizeChanged {
                    screenHeight = Dp(it.height / localDensity.density)
                }
        ) {
            CustomBottomSheet(modifier = Modifier, screenHeight)
        }
    }
}

COLLAPSED

HALF EXPANDED

FULLY EXPANDED

Illsorted answered 28/9 at 23:16 Comment(1)
I moved away from applying LocalConfiguration.current.screenHeightDp because I have a native toolbar. So in this case you can pass screenHeightDp, or measure the height of the hosting component like I did.Illsorted

© 2022 - 2024 — McMap. All rights reserved.