🛠️ New CustomModalDrawer

* Follows Material3
* Does not require to wrap screen's content
This commit is contained in:
Acclorite 2024-09-15 15:36:24 +03:00
parent 57cbdfd057
commit 1c23694d6d
3 changed files with 246 additions and 0 deletions

View file

@ -0,0 +1,140 @@
package ua.acclorite.book_story.presentation.core.components.custom_drawer
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.presentation.core.components.CustomAnimatedVisibility
import ua.acclorite.book_story.presentation.core.components.CustomLazyColumn
import ua.acclorite.book_story.presentation.core.util.noRippleClickable
/**
* Custom Modal Drawer.
* Follows Material3 guidelines. Does not require to wrap screen's content and works independently.
* Do not wrap in if(show) { ... }, use [show].
* Should be placed at the bottom of the composable to be shown at top.
*
* @param show Whether should be shown. Animates enter and exit.
* @param startIndex Start index of the item to show when opening drawer.
* @param onDismissRequest Dismiss callback. Called when dragging, pressing back or clicking outside drawer.
* @param header Header of the drawer. Shown at the top.
* @param content Content inside drawer.
*/
@Composable
fun CustomModalDrawer(
show: Boolean,
startIndex: Int = 0,
onDismissRequest: () -> Unit,
header: @Composable () -> Unit = {},
content: LazyListScope.() -> Unit
) {
val density = LocalDensity.current
val offset = remember { mutableFloatStateOf(0f) }
val offsetDp = remember {
derivedStateOf {
with(density) { offset.floatValue.toDp() }
}
}
val animatedScrimColor = animateFloatAsState(
targetValue = if (show && offsetDp.value > (-60).dp) 0.35f else 0f
)
val animatedOffset = animateDpAsState(
targetValue = offsetDp.value
)
LaunchedEffect(show) {
if (show) offset.floatValue = 0f
}
Box(
Modifier
.fillMaxSize()
.background(Color.Black.copy(animatedScrimColor.value))
.then(
if (!show) Modifier
else Modifier.noRippleClickable(onClick = onDismissRequest)
)
) {
CustomAnimatedVisibility(
visible = show,
enter = slideInHorizontally { (-it + with(density) { -20.dp.toPx() }).toInt() },
exit = slideOutHorizontally { (-it + with(density) { -20.dp.toPx() }).toInt() }
) {
Column(
modifier = Modifier
.offset {
IntOffset(
x = animatedOffset.value
.toPx()
.toInt(),
y = 0
)
}
.fillMaxHeight()
.width(360.dp)
.clip(
MaterialTheme.shapes.large.copy(
topStart = CornerSize(0.dp),
bottomStart = CornerSize(0.dp)
)
)
.background(MaterialTheme.colorScheme.surfaceContainerLow)
.noRippleClickable {}
.pointerInput(Unit) {
detectHorizontalDragGestures(
onDragStart = { offset.floatValue = 0f },
onDragEnd = {
if (offset.floatValue.toDp() < (-60).dp) onDismissRequest()
else offset.floatValue = 0f
}
) { _, dragAmount ->
offset.floatValue = (offset.floatValue + dragAmount)
.coerceAtMost(0f)
}
}
.systemBarsPadding()
) {
header()
CustomLazyColumn(
state = rememberLazyListState(startIndex),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(vertical = 9.dp)
) {
content()
}
}
BackHandler {
onDismissRequest()
}
}
}
}

View file

@ -0,0 +1,61 @@
package ua.acclorite.book_story.presentation.core.components.custom_drawer
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.domain.util.Selected
/**
* Custom Modal Drawer Selectable Item.
* Can be selected.
*
* @param modifier Modifier to be applied.
* @param selected Whether this item is selected.
* @param enabled Whether this item is enabled.
* @param onClick OnClick callback.
* @param content Content of the item.
*/
@Composable
fun CustomModalDrawerSelectableItem(
modifier: Modifier = Modifier,
selected: Selected,
enabled: Boolean = true,
onClick: () -> Unit,
content: @Composable RowScope.() -> Unit
) {
Row(
modifier = modifier
.padding(horizontal = 8.dp)
.fillMaxWidth()
.clip(CircleShape)
.background(
if (selected) MaterialTheme.colorScheme.secondaryContainer
else Color.Transparent
)
.clickable(enabled = enabled) {
onClick()
}
.padding(horizontal = 18.dp, vertical = 18.dp)
) {
CompositionLocalProvider(
LocalContentColor provides if (selected) MaterialTheme.colorScheme.onSecondaryContainer
else MaterialTheme.colorScheme.onSurfaceVariant,
LocalTextStyle provides MaterialTheme.typography.labelLarge
) {
content()
}
}
}

View file

@ -0,0 +1,45 @@
package ua.acclorite.book_story.presentation.core.components.custom_drawer
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* Custom Modal Drawer Title Item.
* Usually displayed at the top of the drawer.
*
* @param modifier Modifier to be applied.
* @param title Title to be shown.
*/
@Composable
fun CustomModalDrawerTitleItem(
modifier: Modifier = Modifier,
title: String
) {
Column(
modifier = modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceContainerLow)
.padding(horizontal = 18.dp)
) {
Spacer(modifier = Modifier.height(9.dp))
Text(
text = title,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(horizontal = 18.dp)
)
Spacer(modifier = Modifier.height(9.dp))
HorizontalDivider()
Spacer(modifier = Modifier.height(4.dp))
}
}