Last active
July 29, 2023 10:12
-
-
Save iamutkarshtiwari/e6ce774aa02499b021193ef6ee0d6db6 to your computer and use it in GitHub Desktop.
Custom Jetpack Compose ModalDrawer to support Left and Right alignments
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.iamutkarshtiwari.ds.component.drawer | |
import androidx.annotation.FloatRange | |
import androidx.compose.foundation.Canvas | |
import androidx.compose.foundation.gestures.Orientation | |
import androidx.compose.foundation.gestures.detectTapGestures | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.foundation.layout.BoxWithConstraints | |
import androidx.compose.foundation.layout.Column | |
import androidx.compose.foundation.layout.ColumnScope | |
import androidx.compose.foundation.layout.fillMaxSize | |
import androidx.compose.foundation.layout.offset | |
import androidx.compose.foundation.layout.padding | |
import androidx.compose.foundation.layout.sizeIn | |
import androidx.compose.material.DrawerDefaults | |
import androidx.compose.material.DrawerState | |
import androidx.compose.material.DrawerValue | |
import androidx.compose.material.ExperimentalMaterialApi | |
import androidx.compose.material.FractionalThreshold | |
import androidx.compose.material.MaterialTheme | |
import androidx.compose.material.Surface | |
import androidx.compose.material.contentColorFor | |
import androidx.compose.material.rememberDrawerState | |
import androidx.compose.material.swipeable | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.rememberCoroutineScope | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.graphics.Shape | |
import androidx.compose.ui.input.pointer.pointerInput | |
import androidx.compose.ui.platform.LocalDensity | |
import androidx.compose.ui.platform.LocalLayoutDirection | |
import androidx.compose.ui.semantics.dismiss | |
import androidx.compose.ui.semantics.paneTitle | |
import androidx.compose.ui.semantics.semantics | |
import androidx.compose.ui.unit.Dp | |
import androidx.compose.ui.unit.IntOffset | |
import androidx.compose.ui.unit.LayoutDirection | |
import androidx.compose.ui.unit.dp | |
import kotlinx.coroutines.launch | |
import kotlin.math.roundToInt | |
/** | |
* NOTE: This is the customized version of [androidx.compose.material.ModalDrawer] to support | |
* drawer pull from both start or end. | |
* | |
* @param modifier optional modifier for the drawer | |
* @param widthFraction Percentage width of screen the drawer occupies | |
* @param drawerDirection Direction (start/end) from which drawer can be pulled out | |
* @param drawerState state of the drawer | |
* @param gesturesEnabled whether or not drawer can be interacted by gestures | |
* @param drawerShape shape of the drawer sheet | |
* @param drawerElevation drawer sheet elevation. This controls the size of the shadow below the | |
* drawer sheet | |
* @param drawerBackgroundColor background color to be used for the drawer sheet | |
* @param drawerContentColor color of the content to use inside the drawer sheet. Defaults to | |
* either the matching content color for [drawerBackgroundColor], or, if it is not a color from | |
* the theme, this will keep the same value set above this Surface. | |
* @param scrimColor color of the scrim that obscures content when the drawer is open | |
* @param drawerContent composable that represents content inside the drawer | |
* @param content content of the rest of the UI | |
* | |
* @throws IllegalStateException when parent has [Float.POSITIVE_INFINITY] width | |
*/ | |
@Composable | |
@OptIn(ExperimentalMaterialApi::class) | |
internal fun ModalDrawer( | |
modifier: Modifier = Modifier, | |
@FloatRange(from = 0.0, to = 1.0, fromInclusive = true) | |
widthFraction: Float = 1f, | |
drawerDirection: Direction = Direction.END, | |
drawerState: DrawerState = rememberDrawerState(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, | |
drawerContent: @Composable ColumnScope.() -> Unit, | |
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 minValue = modalDrawerConstraints.maxWidth.toFloat() * | |
(if (drawerDirection == Direction.START) -1 else 1) | |
val maxValue = 0f | |
val anchors = mapOf(minValue to DrawerValue.Closed, maxValue to DrawerValue.Open) | |
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl | |
val blockClicks = if (drawerState.isOpen) { | |
Modifier.pointerInput(Unit) { detectTapGestures {} } | |
} else { | |
Modifier | |
} | |
Box( | |
Modifier.swipeable( | |
state = drawerState, | |
anchors = anchors, | |
thresholds = { _, _ -> FractionalThreshold(0.5f) }, | |
orientation = Orientation.Horizontal, | |
enabled = gesturesEnabled, | |
reverseDirection = isRtl, | |
velocityThreshold = DrawerVelocityThreshold, | |
resistance = null | |
) | |
) { | |
Box { | |
content() | |
} | |
Scrim( | |
open = drawerState.isOpen, | |
onClose = { scope.launch { drawerState.close() } }, | |
fraction = { calculateFraction(minValue, maxValue, drawerState.offset.value) }, | |
color = scrimColor | |
) | |
val padding = calculatePaddingFromFraction(widthFraction, modalDrawerConstraints.maxWidth) | |
Surface( | |
modifier = with(LocalDensity.current) { | |
Modifier | |
.sizeIn( | |
minWidth = modalDrawerConstraints.minWidth.toDp(), | |
minHeight = modalDrawerConstraints.minHeight.toDp(), | |
maxWidth = modalDrawerConstraints.maxWidth.toDp(), | |
maxHeight = modalDrawerConstraints.maxHeight.toDp() | |
) | |
} | |
.semantics { | |
paneTitle = Strings.NavigationMenu | |
if (drawerState.isOpen) { | |
dismiss(action = { scope.launch { drawerState.close() }; true }) | |
} | |
} | |
.offset { IntOffset(drawerState.offset.value.roundToInt(), 0) } | |
.padding( | |
start = if (drawerDirection == Direction.END) padding else 0.dp, | |
end = if (drawerDirection == Direction.START) padding else 0.dp | |
), | |
shape = drawerShape, | |
color = drawerBackgroundColor, | |
contentColor = drawerContentColor, | |
elevation = drawerElevation | |
) { | |
Column(Modifier.fillMaxSize().then(blockClicks), content = drawerContent) | |
} | |
} | |
} | |
} | |
@Composable | |
private fun Scrim( | |
open: Boolean, | |
onClose: () -> Unit, | |
fraction: () -> Float, | |
color: Color | |
) { | |
val dismissDrawer = if (open) { | |
Modifier.pointerInput(onClose) { detectTapGestures { onClose() } } | |
} else { | |
Modifier | |
} | |
Canvas( | |
Modifier | |
.fillMaxSize() | |
.then(dismissDrawer) | |
) { | |
drawRect(color, alpha = fraction()) | |
} | |
} | |
enum class Direction { | |
START, | |
END | |
} | |
@Composable | |
private fun calculatePaddingFromFraction( | |
@FloatRange(from = 0.0, to = 1.0, fromInclusive = true) | |
fraction: Float, | |
maxWidth: Int | |
): Dp { | |
return (((1f - fraction) * maxWidth) / LocalDensity.current.density).dp | |
} | |
private fun calculateFraction(a: Float, b: Float, pos: Float) = | |
((pos - a) / (b - a)).coerceIn(0f, 1f) | |
private val DrawerVelocityThreshold = 400.dp | |
internal object Strings { | |
const val NavigationMenu = "Navigation menu" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
There is an alternative solution:
https://github.com/LeviOrtega/feedback-prototype/blob/aed5bbe69a0061000e90cc5422ac491e4d036147/app/src/main/java/com/example/feedback/ui/pages/components/Drawer.kt