Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iamutkarshtiwari/e6ce774aa02499b021193ef6ee0d6db6 to your computer and use it in GitHub Desktop.
Save iamutkarshtiwari/e6ce774aa02499b021193ef6ee0d6db6 to your computer and use it in GitHub Desktop.
Custom Jetpack Compose ModalDrawer to support Left and Right alignments
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"
}
@akwasiio
Copy link

@iamutkarshtiwari this doesn't work on compose 1.0.1. Can you please revise it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment