当前位置: 首页 > 知识库问答 >
问题:

如何在同一个屏幕上添加两个模态抽屉?

弓华茂
2023-03-14

我可以毫无问题地实现模态抽屉。但是我想在同一个屏幕上添加两个模态抽屉,一个从左边,一个从右边。在xml中很容易做到,但是我不知道如何在Jetpack中做到这一点。

    val navController = rememberNavController()
    Surface(color = MaterialTheme.colors.background) {
        val drawerState = rememberDrawerState(DrawerValue.Closed)
        val scope = rememberCoroutineScope()
        val openDrawer = {
            scope.launch {
                drawerState.open()
            }
        }
        //CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl ) {
            ModalDrawer(drawerState = drawerState, gesturesEnabled = true,//drawerState.isOpen,
                drawerContent = {
                    Drawer(onDestinationClicked = { route ->
                        scope.launch {
                            drawerState.close()
                        }
                        Handler(Looper.getMainLooper()).postDelayed({
                            navController.navigate(route) {
                                //popUpTo = navController.graph.startDestination
                                //popUpTo = navController.graph.startDestinationId
                                popUpTo(navController.graph.startDestinationId)
                                launchSingleTop = true
                            }
                        }, 100)
                    }
                    )
                }
            ) {
                //CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr ) {
                    NavHost(navController = navController, startDestination = DrawerScreens.Home.route) {
                        composable(DrawerScreens.Home.route) {
                            Home(openDrawer = { openDrawer() })
                        }
                        composable(DrawerScreens.Account.route) {
                            Account(openDrawer = { openDrawer() })
                        }
                        composable(DrawerScreens.Help.route) {
                            Help(navController = navController)
                        }
                    }
                //}

            }
        //}
    }

共有1个答案

韩博简
2023-03-14

我找不到任何内置解决方案。因此,我编写了自己的双模态抽屉实现。

双抽屉布局

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.VectorConverter
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.consumeAllChanges
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import inn.rachika.ram2.common.lib.WriteLog
import inn.rachika.ram2.presentation.list.memberList.MemberListScreen
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.roundToInt
import kotlin.random.Random

class DoubleDrawerState(){
    var scope = CoroutineScope(Dispatchers.Main)
    var maxWidth = 0f
    var leftOffsetX = Animatable(-5000f)/*initial value is to prevent the drawer to show during startup*/
    var leftDrawerWidthPx = 0f
    var leftThreshold = 0f
    var isLeftOpen = false
    var leftDrawerEnabled = true

    var rightDrawerWidthPx = 0f
    var rightOffsetX = Animatable(5000f)
    var rightThreshold = 0f
    var isRightOpen = false
    var rightDrawerEnabled = true

    var fiftyPercentL = 0f
    var fiftyPercentR = 0f

    private var isLeftDragging = false
    private var isRightDragging = false
    private var velocity = 0f
    private val velocityThreshold = 25f

    internal fun onDrag(delta: Float){
        if ((!leftOffsetX.isRunning) && (!rightOffsetX.isRunning)) {
            velocity = delta
            when {
                delta > 0 -> {/* left drawer */
                    when {
                        isRightDragging -> {
                            onDragRight(delta)
                        }
                        else -> {
                            if(!isRightOpen){ isLeftDragging = true }/*if right drawer is not open then capture dragging*/
                            if (isRightOpen) onDragRight(delta) else onDragLeft(delta)
                        }
                    }
                }
                delta < 0 -> {/* right drawer */
                    when {
                        isLeftDragging -> {
                            onDragLeft(delta)
                        }
                        else -> {
                            if(!isLeftOpen){ isRightDragging = true }/*if left drawer is not open then capture dragging*/
                            if (isLeftOpen) onDragLeft(delta) else onDragRight(delta)
                        }
                    }
                }
            }
        }
    }

    internal fun onDragEnd(){
        if(abs(velocity) > velocityThreshold){
            performFling(velocity)
            return
        } else{
            if (isRightOpen) {
                when {
                    rightOffsetX.value > (rightThreshold + fiftyPercentR) -> hideRight()
                    else -> showRight()
                }
            } else {
                when {
                    rightOffsetX.value > (maxWidth - fiftyPercentR) -> hideRight()
                    rightOffsetX.value < (maxWidth - fiftyPercentR) -> showRight()
                }
            }

            if (isLeftOpen) {
                /*hide if 20% swipe in*/
                when {
                    abs(leftOffsetX.value) > (abs(leftThreshold) + fiftyPercentL) -> hideLeft()
                    else -> showLeft()
                }
            } else {
                /*show if 20% swipe out*/
                when {
                    abs(leftOffsetX.value) < (maxWidth - fiftyPercentL) -> showLeft()
                    else -> hideLeft()
                }
            }
            isLeftDragging = false
            isRightDragging = false
        }

        //WriteLog("Pointer Velocity: $velocity  ------- ${Random.nextInt(0, 100)}")
        velocity = 0f

    }

    private fun performFling(velocity: Float){
        if(velocity > velocityThreshold){
            if(isRightOpen) hideRight() else showLeft()
        }
        if(velocity < velocityThreshold){
            if(isLeftOpen) hideLeft() else showRight()
        }
    }

    private fun onDragLeft(delta: Float){
        if(!leftDrawerEnabled){ return }
        when {
            (leftOffsetX.value + delta) > leftThreshold -> {
                scope.launch { leftOffsetX.snapTo(leftThreshold) }
                isLeftOpen = true
            }
            (leftOffsetX.value + delta) < -maxWidth -> {
                scope.launch { leftOffsetX.snapTo(-maxWidth) }
                isLeftOpen = false
            }
            else -> {
                scope.launch { leftOffsetX.snapTo(leftOffsetX.value + delta) }
            }
        }
    }

    private fun onDragRight(delta: Float){
        if(!rightDrawerEnabled){ return }
        when {
            (rightOffsetX.value + delta) <= rightThreshold -> {
                scope.launch { rightOffsetX.snapTo(rightThreshold) }
                isRightOpen = true
            }
            (rightOffsetX.value + delta) > maxWidth -> {
                scope.launch { rightOffsetX.snapTo(maxWidth) }
                isRightOpen = false
            }
            else -> {
                scope.launch { rightOffsetX.snapTo(rightOffsetX.value + delta) }
            }
        }
    }


    fun showLeft(){
        if(!leftDrawerEnabled){ return }
        scope.launch {
            rightOffsetX.snapTo(maxWidth)/*hide right first*/
            leftOffsetX.animateTo(leftThreshold)/*then show left*/
        }
        if(!leftOffsetX.isRunning){ isLeftOpen = true }
    }
    fun hideLeft(){
        scope.launch {
            leftOffsetX.animateTo(-maxWidth)
        }
        if(!leftOffsetX.isRunning){ isLeftOpen = false }
    }
    fun showRight(){
        if(!rightDrawerEnabled){ return }
        scope.launch {
            leftOffsetX.snapTo(-maxWidth)/*hide left first*/
            rightOffsetX.animateTo(rightThreshold)/*then show right*/
        }
        if(!rightOffsetX.isRunning){ isRightOpen = true }
    }
    fun hideRight(){
        scope.launch {
            rightOffsetX.animateTo(maxWidth)
        }
        if(!rightOffsetX.isRunning){ isRightOpen = false }
    }

    internal fun onConfigurationChange(){
        WriteLog("leftThreshold: $leftThreshold")
        if(isLeftOpen){
            scope.launch {leftOffsetX.snapTo(leftThreshold)}
        } else{
            scope.launch {leftOffsetX.snapTo(-maxWidth)}
        }
        if(isRightOpen){
            scope.launch { rightOffsetX.snapTo(rightThreshold) }
        } else{
            scope.launch { rightOffsetX.snapTo(maxWidth) }
        }
    }

    companion object{
        val OffsetSaver = listSaver<Animatable<Float, AnimationVector1D>, Any>(
            save = { listOf(it.value)},
            restore = { Animatable(it[0] as Float, Float.VectorConverter, Spring.DefaultDisplacementThreshold) }
        )
    }
}

@Composable
fun DoubleDrawerLayout(
    state: DoubleDrawerState = remember{ DoubleDrawerState() },
    leftDrawerWidth: Dp = 300.dp,
    leftDrawerContent: @Composable BoxScope.() -> Unit = { LeftDrawerTestContent { state.hideLeft() } },
    leftDrawerEnabled: Boolean = true,
    rightDrawerWidth: Dp = 250.dp,
    rightDrawerContent: @Composable BoxScope.() -> Unit = { RightDrawerTestContent { state.hideRight() } },
    rightDrawerEnabled: Boolean = true,
    body: @Composable BoxScope.() -> Unit = { TestBody() }
) {
    val scope = rememberCoroutineScope()
    state.scope = scope

    BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
        val constraintScope = this
        val density = LocalDensity.current
        state.maxWidth = with(density){ constraintScope.maxWidth.toPx()}
        LaunchedEffect(Unit) {
            state.leftOffsetX.snapTo(-state.maxWidth)
            state.rightOffsetX.snapTo(state.maxWidth)
        }

        state.leftDrawerWidthPx = with(density){ (leftDrawerWidth).toPx()}
        state.leftThreshold = -(state.maxWidth - state.leftDrawerWidthPx)
        state.leftDrawerEnabled = leftDrawerEnabled

        state.rightDrawerWidthPx = with(density){ (rightDrawerWidth).toPx()}
        state.rightThreshold = (state.maxWidth - state.rightDrawerWidthPx)
        state.rightDrawerEnabled = rightDrawerEnabled

        state.fiftyPercentL = (state.leftDrawerWidthPx * 50) / 100
        state.fiftyPercentR = (state.rightDrawerWidthPx * 50) / 100

        Box(
            modifier = Modifier
                .matchParentSize()
                .pointerInput(Unit) {
                    detectDragGestures(
                        onDrag = { change: PointerInputChange, dragAmount: Offset ->
                            change.consumeAllChanges()
                            state.onDrag(dragAmount.x)
                        },
                        onDragEnd = {
                            state.onDragEnd()
                        }
                    )
                }
        ){
            MemberListScreen()(vm = hiltViewModel(), {}, { photoId, personId ->  })
            //body()

            LeftDrawerBody(
                leftOffsetX = state.leftOffsetX.value,
                leftThreshold = state.leftThreshold,
                leftDrawerWidth = leftDrawerWidth,
                onHideRequest = {state.hideLeft()},
                content = leftDrawerContent
            )

            RightDrawerBody(
                rightOffsetX = state.rightOffsetX.value,
                rightThreshold = state.rightThreshold,
                rightDrawerWidth = rightDrawerWidth,
                onHideRequest = {state.hideRight()},
                content = rightDrawerContent
            )
        }
    }
}



@Composable
private fun LeftDrawerBody(
    leftOffsetX: Float,
    leftThreshold: Float,
    leftDrawerWidth: Dp,
    onHideRequest: () -> Unit,
    content: @Composable BoxScope.() -> Unit
) {
    Scrim(
        open = leftOffsetX >= leftThreshold,
        onClose = { onHideRequest() },
        opacity = {0f},
        color = Color.Transparent//DrawerDefaults.scrimColor
    )
    Surface(
        modifier = Modifier
            .fillMaxSize()
            .offset { IntOffset(x = leftOffsetX.roundToInt(), y = 0) }
    ) {
        Box(modifier = Modifier.fillMaxSize()){
            Box(
                modifier = Modifier
                    .fillMaxHeight()
                    .width(leftDrawerWidth)
                    .border(width = 5.dp, color = Color.Yellow)
                    .align(Alignment.TopEnd)
            ){
                content()
            }
        }
    }
}

@Composable
private fun RightDrawerBody(
    rightOffsetX: Float,
    rightThreshold: Float,
    rightDrawerWidth: Dp,
    onHideRequest: () -> Unit,
    content: @Composable BoxScope.() -> Unit
) {
    Scrim(
        open = rightOffsetX == rightThreshold,
        onClose = { onHideRequest() },
        opacity = {0f},
        color = Color.Transparent//DrawerDefaults.scrimColor
    )
    Surface(
        modifier = Modifier
            .fillMaxSize()
            .offset { IntOffset(x = rightOffsetX.roundToInt(), y = 0) }
    ) {
        Box(modifier = Modifier.fillMaxSize()){
            Box(
                modifier = Modifier
                    .fillMaxHeight()
                    .width(rightDrawerWidth)
                    .border(width = 5.dp, color = Color.Yellow)
                    .align(Alignment.TopStart)
            ){
                content()
            }
        }
    }
}

@Composable
private fun TestBody() {
    LazyColumn(modifier = Modifier.fillMaxSize()){
        for(i in 1..50){
            item {
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    verticalAlignment = Alignment.CenterVertically
                ){
                    OutlinedButton(
                        onClick = {},
                        content = { Text(text = "Double!") },
                        modifier = Modifier.padding(start = 16.dp)
                    )
                    Text(
                        text = "#$i. Double Drawer Layout...",
                        modifier = Modifier
                            .weight(1f)
                            .padding(start = 16.dp)

                    )
                    OutlinedButton(
                        onClick = {},
                        content = { Text(text = "Click Me!") },
                        modifier = Modifier.padding(end = 16.dp)
                    )
                }
                Divider()
            }
        }
    }
}

@Composable
private fun BoxScope.LeftDrawerTestContent(onHideRequest: () -> Unit) {
    Button(
        onClick = { onHideRequest() },
        content = { Text(text = "#Click to hide!") },
        modifier = Modifier.align(Alignment.Center)
    )
}

@Composable
private fun BoxScope.RightDrawerTestContent(onHideRequest: () -> Unit) {
    Button(
        onClick = { onHideRequest() },
        content = { Text(text = "Click to hide!") },
        modifier = Modifier.align(Alignment.Center)
    )
}

@Composable
private fun Scrim(
    open: Boolean,
    onClose: () -> Unit,
    opacity: () -> Float,
    color: Color
) {
    val closeDrawer = "getString(Strings.CloseDrawer)"
    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 = opacity())
    }
}
 类似资料:
  • 问题内容: 我有两个桌子:玩具和游戏。 一个小孩可以有多个玩具。一个小孩可以一次参加多个游戏。 我想要一个查询,该查询将为我提供little_kid涉及的玩具和游戏的总数。 基本上,我想要这两个查询的总和: 是否可以在单个SQL查询中获得它?显然,我可以通过编程的方式对它们进行汇总,但这并不是很理想。 (我意识到人为的例子使模式看起来效率低下。让我们假设我们无法更改模式。) 问题答案: 包装它们并

  • 我在写一个程序,允许用户通过菜单选项画出不同的形状,画出后的形状需要在同一屏幕上,但问题是在菜单中选择另一个选项画出另一个形状后,前一个形状就消失了。我该怎么解决这个?这是我的程序: 仅在中。

  • 问题内容: 如何从一个活动屏幕导航到另一个活动屏幕?在第一个屏幕中,我有一个按钮,如果我单击该按钮,则必须移至另一个“活动”屏幕。 问题答案:

  • 问题内容: 每当我使用外键指向对象B编辑对象A时,在对象B的选择旁边都会有一个加号选项“添加另一个”。如何删除该选项? 我配置了没有权限添加对象B的用户。加号仍然可用,但是当我单击它时,它显示“权限被拒绝”。它很丑。 我正在使用Django 1.0.2 问题答案: 不建议的答案 从那以后,Django使这成为可能。 您是否考虑过使用CSS来简单地不显示按钮?也许有点太过分了。 这未经测试,但我在想

  • 问题内容: 我有两个数字。例如: 为什么数字不支持算术运算?无论如何,我将如何在Java中将这两个数字相加?(当然,我是从某个地方获取它们的,我不知道它们是Integer还是float等)。 问题答案: 你不知道数字是整数还是浮点数…使用该类时,编译器也不知道你的数字是整数,浮点数还是其他东西。结果,像+和-这样的基本数学运算符不起作用;计算机将不知道如何处理这些值。 开始编辑 根据讨论,我认为一

  • 下面的代码是一个按钮的方法。 它总是给我一个错误。 如果我删除,那么它可以正常工作。 当我单击按钮时,如何使用POST方法,并移动到另一个活动? 错误:04-27 19:41:26.066 197 4-1974/com.opshun_test.opshun_test e/androidruntime:致命异常:main process:com.opshun_test.opshun_test,pid