【Jetpack compose】Drawer相关问题

484 阅读2分钟

1. BottomDrawer、ModalBottomSheetLayout 怎么跳过HalfExpanded

ModalBottomSheetLayout:

如果compose是1.2.0及以上版本

val bottomSheetStatus =
    rememberModalBottomSheetState(
        ModalBottomSheetValue.Hidden,
        skipHalfExpanded = true,
    )

compose是1.1.1版本:

 val bottomSheetState =
        rememberModalBottomSheetState(ModalBottomSheetValue.Hidden, confirmStateChange = {
            it != ModalBottomSheetValue.HalfExpanded
        })
scope.launch {
    if(bottomSheetState.isVisible) bottomSheetState.hide() else  bottomSheetState.animateTo(ModalBottomSheetValue.Expanded)
}

BottomDrawer:

val drawerState: BottomDrawerState = rememberBottomDrawerState(BottomDrawerValue.Closed){
    it != BottomDrawerValue.Open
}
scope.launch {
    if (drawerState.isClosed) drawerState.expand() else drawerState.close()
}

ModalDrawer 从右侧往左侧打开

1. 从stackoverflow找到的一个解决方案

CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl ) {
    ModalDrawer(
        drawerState = drawerState,
        drawerContent = { /* ...*/ },
        content = { /* ..*/ }
    )
}

但是用这个方法会导致drawerContent 和 content的内容是从右向左布局的

可以在content 再反过来:

CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl ) {
    ModalDrawer(
        drawerState = drawerState,
        drawerContent = { 
        CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr
 ) {/* ...*/ }},
        content = {CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr
 ) {/* ...*/ } }
    )
}

这样做还是会碰到一些奇怪的问题。

2. Copy ModalDrawer 自定义一个RightModalDrawer


@Composable
@OptIn(ExperimentalMaterialApi::class)
fun RightModalDrawer(
    modifier: Modifier = Modifier,
    drawerState: MyDrawerState = rememberMyDrawerState(DrawerValue.Closed),
    gesturesEnabled: Boolean = true,
    drawerContent: @Composable ColumnScope.() -> Unit,
    @FloatRange(from = 0.0, to = 1.0) drawerPercent: Float = 0.8f,
    drawerShape: Shape = MaterialTheme.shapes.large,
    drawerElevation: Dp = DrawerDefaults.Elevation,
    drawerBackgroundColor: Color = MaterialTheme.colors.surface,
    drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
    scrimColor: Color = DrawerDefaults.scrimColor,
    content: @Composable () -> Unit
) {
    val scope = rememberCoroutineScope()
    BoxWithConstraints(modifier.fillMaxSize()) {
        val modalDrawerConstraints = constraints
        // TODO : think about Infinite max bounds case
        if (!modalDrawerConstraints.hasBoundedWidth) {
            throw IllegalStateException("Drawer shouldn't have infinite width")
        }

        val fullWidth = constraints.maxWidth.toFloat()
        val drawerWidth = fullWidth * drawerPercent

        val maxValue = kotlin.math.max(0f, fullWidth - drawerWidth)

        val anchors = mapOf(fullWidth to DrawerValue.Closed, maxValue to DrawerValue.Open)
        Box(
            Modifier.swipeable(
                state = drawerState,
                anchors = anchors,
                thresholds = { _, _ -> FractionalThreshold(0.5f) },
                orientation = Orientation.Horizontal,
                enabled = gesturesEnabled,
                velocityThreshold = fullWidth.dp,
                resistance = null
            )
        ) {
            Box {
                content()
            }
            Scrim(
                open = drawerState.isOpen,
                onClose = {
                    if (
                        gesturesEnabled &&
                        drawerState.confirmStateChange(DrawerValue.Closed)
                    ) {
                        scope.launch { drawerState.close() }
                    }
                },
                fraction = {
                    calculateFraction(fullWidth, maxValue, drawerState.offset.value)
                },
                color = scrimColor
            )
            Surface(
                modifier = with(LocalDensity.current) {
                    Modifier
                        .sizeIn(
                            maxWidth = drawerWidth.toDp(),
                            maxHeight = modalDrawerConstraints.maxHeight.toDp()
                        )
                }
                    .offset { IntOffset(drawerState.offset.value.roundToInt(), 0) }
                    .semantics {
                        paneTitle = "navigationMenu"
                        if (drawerState.isOpen) {
                            dismiss {
                                if (drawerState.confirmStateChange(DrawerValue.Closed)) {
                                    scope.launch { drawerState.close() }
                                }
                                true
                            }
                        }
                    },
                shape = drawerShape,
                color = drawerBackgroundColor,
                contentColor = drawerContentColor,
                elevation = drawerElevation
            ) {
                Column(Modifier.fillMaxSize(), content = drawerContent)
            }
        }
    }
}

3. Drawer 从上往下打开

一样copy BottomDrawer的代码 然后自定义一个

@Composable
@ExperimentalMaterialApi
fun TopDrawer(
    drawerContent: @Composable ColumnScope.() -> Unit,
    modifier: Modifier = Modifier,
    drawerState: MyDrawerState = rememberMyDrawerState(DrawerValue.Closed),
    gesturesEnabled: Boolean = true,
    drawerShape: Shape = MaterialTheme.shapes.large,
    drawerElevation: Dp = DrawerDefaults.Elevation,
    drawerBackgroundColor: Color = MaterialTheme.colors.surface,
    drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
    scrimColor: Color = DrawerDefaults.scrimColor,
    content: @Composable () -> Unit
) {
    val scope = rememberCoroutineScope()
    BoxWithConstraints(modifier.fillMaxSize()) {
        val fullHeight = constraints.maxHeight.toFloat()
        //var drawerHeight by remember(fullHeight) { mutableStateOf(fullHeight) }

        val minHeight = 0f
        //val expandedHeight = max(minHeight, fullHeight - drawerHeight)
        val minValue = minHeight - fullHeight
        val maxValue = minHeight
        val anchors =
            mapOf(
                minValue to DrawerValue.Closed,
                maxValue to DrawerValue.Open
            )
        val drawerConstraints = with(LocalDensity.current) {
            Modifier
                .sizeIn(
                    maxWidth = constraints.maxWidth.toDp(),
                    maxHeight = constraints.maxHeight.toDp()
                )
        }
        Box(
            Modifier
                //.then(nestedScroll)
                .swipeable(
                    state = drawerState,
                    anchors = anchors,
                    orientation = Orientation.Vertical,
                    enabled = gesturesEnabled,
                    resistance = null
                )
        ) {
            content()
            Scrim(
                open = drawerState.isOpen,
                onClose = {
                    if (
                        /*gesturesEnabled && */drawerState.confirmStateChange(DrawerValue.Closed)
                    ) {
                        scope.launch { drawerState.close() }
                    }
                },
                fraction = {
                    calculateFraction(minValue, maxValue, drawerState.offset.value)
                },
                color = scrimColor,
            )
            Surface(
                drawerConstraints
                    .offset { IntOffset(x = 0, y = drawerState.offset.value.roundToInt()) }
                    /*.onGloballyPositioned { position ->
                        drawerHeight = position.size.height.toFloat()
                    }*/
                    .semantics {
                        paneTitle = "navigationMenu"
                        if (drawerState.isOpen) {
                            dismiss {
                                if (drawerState.confirmStateChange(DrawerValue.Closed)) {
                                    scope.launch { drawerState.close() }
                                }
                                true
                            }
                        }
                    },
                shape = drawerShape,
                color = drawerBackgroundColor,
                contentColor = drawerContentColor,
                elevation = drawerElevation
            ) {
                Column(content = drawerContent)
            }
        }
    }
}
@Composable
@ExperimentalMaterialApi
fun rememberMyDrawerState(
    initialValue: DrawerValue,
    confirmStateChange: (DrawerValue) -> Boolean = { true }
): MyDrawerState {
    return rememberSaveable(saver = MyDrawerState.Saver(confirmStateChange)) {
        MyDrawerState(initialValue, confirmStateChange)
    }
}

@ExperimentalMaterialApi
class MyDrawerState(
    initialValue: DrawerValue,
    internal val confirmStateChange: (DrawerValue) -> Boolean = { true }
) : SwipeableState<DrawerValue>(
    initialValue = initialValue,
    confirmStateChange = confirmStateChange
) {
    /**
     * Whether the drawer is open.
     */
    val isOpen: Boolean
        get() = currentValue == DrawerValue.Open

    /**
     * Whether the drawer is closed.
     */
    val isClosed: Boolean
        get() = currentValue == DrawerValue.Closed


    suspend fun open() = animateTo(DrawerValue.Open)

    suspend fun close() = animateTo(DrawerValue.Closed)

    companion object {
        /**
         * The default [Saver] implementation for [BottomDrawerState].
         */
        fun Saver(confirmStateChange: (DrawerValue) -> Boolean) =
            Saver<MyDrawerState, DrawerValue>(
                save = { it.currentValue },
                restore = { MyDrawerState(it, confirmStateChange) }
            )
    }
}

@Composable
private fun Scrim(
    open: Boolean,
    onClose: () -> Unit,
    fraction: () -> Float,
    color: Color
) {
    val dismissDrawer = if (open) {
        Modifier
            .pointerInput(onClose) { detectTapGestures { onClose() } }
            .semantics(mergeDescendants = true) {
                contentDescription = "closeDrawer"
                onClick { onClose(); true }
            }
    } else {
        Modifier
    }

    Canvas(
        Modifier
            .fillMaxSize()
            .then(dismissDrawer)
    ) {
        drawRect(color, alpha = fraction())
    }
}

private fun calculateFraction(a: Float, b: Float, pos: Float) =
    ((pos - a) / (b - a)).coerceIn(0f, 1f)