Skip to content

Instantly share code, notes, and snippets.

@tadfisher
Created April 29, 2022 22:43
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tadfisher/f10639b8dbf2cb96ea5cd448b5c48660 to your computer and use it in GitHub Desktop.
Save tadfisher/f10639b8dbf2cb96ea5cd448b5c48660 to your computer and use it in GitHub Desktop.
BottomSheetHost
object BottomSheetDefaults {
@Stable
val Elevation = 16.dp
@Stable
val OuterPadding: PaddingValues = PaddingValues(top = 48.dp)
@Stable
val ContentPadding: PaddingValues
@Composable
get() = WindowInsets.navigationBars.union(WindowInsets.ime)
.asPaddingValues()
.plus(
start = 16.dp,
top = 24.dp,
end = 16.dp,
bottom = 16.dp
)
@Stable
val Shape: Shape
@Composable
get() = MaterialTheme.shapes.large.copy(
bottomStart = ZeroCornerSize,
bottomEnd = ZeroCornerSize
)
}
interface BottomSheetScope : ColumnScope {
val bottomSheetState: ModalBottomSheetState
}
@Stable
internal class BottomSheetScopeImpl(
override val bottomSheetState: ModalBottomSheetState,
private val columnScope: ColumnScope
) : BottomSheetScope, ColumnScope by columnScope
@Stable
internal class BottomSheetData(
val state: ModalBottomSheetState,
shape: Shape,
elevation: Dp,
backgroundColor: Color,
contentColor: Color,
scrimColor: Color
) {
var shape: Shape by mutableStateOf(shape)
var elevation: Dp by mutableStateOf(elevation)
var backgroundColor: Color by mutableStateOf(backgroundColor)
var contentColor: Color by mutableStateOf(contentColor)
var scrimColor: Color by mutableStateOf(scrimColor)
var onDismiss: () -> Unit = {}
var content: @Composable BottomSheetScope.() -> Unit = {}
val isVisible by derivedStateOf {
state.currentValue != ModalBottomSheetValue.Hidden ||
state.targetValue != ModalBottomSheetValue.Hidden
}
suspend fun dismiss() {
state.hide()
}
}
@Stable
class BottomSheetHostState{
internal var currentBottomSheet by mutableStateOf<BottomSheetData?>(null)
private set
internal suspend fun show(bottomSheet: BottomSheetData) {
try {
hide()
} finally {
currentBottomSheet = bottomSheet
bottomSheet.state.show()
}
}
internal suspend fun hide() {
val bottomSheet = currentBottomSheet ?: return
try {
if (bottomSheet.isVisible) {
bottomSheet.state.hide()
}
} finally {
currentBottomSheet = null
}
}
}
val LocalBottomSheetHostState = staticCompositionLocalOf<BottomSheetHostState> {
noLocalProvidedFor("LocalBottomSheetHostState")
}
@Composable
fun BottomSheetHost(
state: BottomSheetHostState = remember { BottomSheetHostState() },
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Box(modifier.fillMaxSize()) {
CompositionLocalProvider(LocalBottomSheetHostState provides state) {
content()
}
val bottomSheet = state.currentBottomSheet
if (bottomSheet != null) {
ModalBottomSheetLayout(
sheetContent = {
bottomSheet.content(BottomSheetScopeImpl(bottomSheet.state, this))
},
sheetState = bottomSheet.state,
sheetShape = bottomSheet.shape,
sheetElevation = bottomSheet.elevation,
sheetBackgroundColor = bottomSheet.backgroundColor,
sheetContentColor = bottomSheet.contentColor,
scrimColor = bottomSheet.scrimColor,
content = {}
)
var wasShown by remember { mutableStateOf(false) }
DisposableEffect(bottomSheet.isVisible) {
if (bottomSheet.isVisible) {
wasShown = true
} else if (wasShown) {
bottomSheet.onDismiss()
}
onDispose { }
}
}
}
}
@Composable
fun BottomSheet(
state: ModalBottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
skipHalfExpanded = true,
),
onDismiss: () -> Unit = { },
hostState: BottomSheetHostState = LocalBottomSheetHostState.current,
shape: Shape = MaterialTheme.shapes.large,
elevation: Dp = ModalBottomSheetDefaults.Elevation,
backgroundColor: Color = MaterialTheme.colors.surface,
contentColor: Color = MaterialTheme.colors.onSurface,
scrimColor: Color = ScrimDefaults.Color,
content: @Composable BottomSheetScope.() -> Unit
) {
val coroutineScope = rememberCoroutineScope()
val movableContent = remember(content as Any) {
movableContentWithReceiverOf(content)
}
val currentOnDismiss by rememberUpdatedState(onDismiss)
val bottomSheetData = remember {
BottomSheetData(
state = state,
shape = shape,
elevation = elevation,
backgroundColor = backgroundColor,
contentColor = contentColor,
scrimColor = scrimColor,
)
}
SideEffect {
bottomSheetData.shape = shape
bottomSheetData.elevation = elevation
bottomSheetData.backgroundColor = backgroundColor
bottomSheetData.contentColor = contentColor
bottomSheetData.scrimColor = scrimColor
bottomSheetData.onDismiss = currentOnDismiss
bottomSheetData.content = movableContent
}
BackHandler(enabled = bottomSheetData.isVisible) {
coroutineScope.launch {
state.hide()
}
}
LaunchedEffect(hostState, bottomSheetData) {
hostState.show(bottomSheetData)
try {
awaitCancellation()
} finally {
withContext(NonCancellable) {
hostState.hide()
}
}
}
}
@Composable
fun Scrim(
modifier: Modifier = Modifier,
visible: Boolean = true,
clickLabel: String? = null,
onClick: (() -> Unit)? = null,
color: Color = ScrimDefaults.Color,
animationSpec: FiniteAnimationSpec<Float> = tween()
) {
val clickModifier = if (visible && onClick != null) {
Modifier
.pointerInput(onClick) {
detectTapGestures { onClick() }
}
.semantics(mergeDescendants = true) {
onClick(clickLabel) {
onClick()
true
}
}
} else {
Modifier
}
val visibleState = remember { MutableTransitionState(false) }
visibleState.targetState = visible
val transition = updateTransition(visibleState, "ScrimVisibility")
val alpha by transition.animateFloat(
transitionSpec = { animationSpec },
label = "ScrimAlpha"
) { isVisible -> if (isVisible) 1f else 0f }
Canvas(
modifier
.fillMaxSize()
.then(clickModifier)
) {
drawRect(color = color, alpha = alpha)
}
}
object ScrimDefaults {
val Color: Color
@Composable
get() {
val colors = MaterialTheme.colors
return if (colors.isLight) {
colors.onSurface.copy(alpha = ContentAlpha.medium)
} else {
colors.surface.copy(alpha = ContentAlpha.high)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment