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



我能够实现模态抽屉没有问题。但我想添加两个Modal Drawer,一个来自左侧,一个来自右侧,在同一个屏幕中。这在xml中很容易做到,但我无法弄清楚如何在Jetpack Compose中做到这一点。

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)
}
}
//}
}
//}
}

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

DoubleDrawerLayout

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
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())
}
}

最新更新