Skip to content

Instantly share code, notes, and snippets.

@LouisCAD
Last active November 20, 2023 09:16
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save LouisCAD/bebbe3a1e91be4efac94357acf5ef7b8 to your computer and use it in GitHub Desktop.
Save LouisCAD/bebbe3a1e91be4efac94357acf5ef7b8 to your computer and use it in GitHub Desktop.
Put this in its own Gradle module to have ModalBottomSheet in Material3 (or even without Material Design at all).
// Uses refreshVersions, the easiest way to add KotlinX and AndroidX libs, AND keep them up to date.
// See https://github.com/jmfayard/refreshVersions
import de.fayard.refreshVersions.core.versionFor
plugins {
kotlin("android")
id("com.android.library")
}
android {
namespace = "com.louiscad.splitties.ui.bottomsheet"
buildFeatures.compose = true
composeOptions {
kotlinCompilerExtensionVersion = versionFor(AndroidX.compose.compiler)
}
//region Boilerplate (put it in a precompiled script if possible)
compileSdk = 33
defaultConfig {
minSdk = 23
}
buildTypes {
getByName("release").isMinifyEnabled = false
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
//endregion
}
dependencies {
api(AndroidX.compose.runtime)
api(AndroidX.compose.ui)
implementation(AndroidX.compose.material)
implementation(AndroidX.activity.compose)
}
package com.louiscad.splitties.eap.bottomsheet
import android.app.Activity
import android.view.ViewGroup
import androidx.activity.compose.BackHandler
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCompositionContext
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import kotlinx.coroutines.*
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ModalBottomSheet(
modalState: ModalState,
content: @Composable () -> Unit
) {
val context = LocalContext.current
val contentView: ViewGroup = remember {
val activity = context as Activity
activity.findViewById(android.R.id.content) as ViewGroup
}
val coroutineScope = rememberCoroutineScope()
val compositionContext = rememberCompositionContext()
val internalState: ModalStateImpl
val state = when (modalState) {
is ModalStateImpl -> {
internalState = modalState
modalState._state
}
}
val shouldBeVisible =
modalState.isAtLeastPartiallyVisible || internalState.hasPendingShowRequests
var lastComposeView: ComposeView? by remember { mutableStateOf(null) }
val composeView = remember(shouldBeVisible, compositionContext) {
lastComposeView?.let { contentView.removeView(it) }
val newView: ComposeView? = when {
shouldBeVisible -> ComposeView(contentView.context).also {
it.setParentCompositionContext(compositionContext)
contentView.addView(it)
}
else -> {
null
}
}
lastComposeView = newView
newView
}
DisposableEffect(composeView, content as Any) {
composeView?.setContent {
ModalBottomSheetLayout(
sheetBackgroundColor = Color.Transparent,
sheetState = state,
sheetContent = {
content()
}
) {}
}
onDispose {}
}
DisposableEffect(Unit) {
onDispose { composeView?.let { contentView.removeView(it) } }
}
BackHandler(enabled = state.isShownOrShowing) {
coroutineScope.launch { modalState.hide() }
}
}
package com.louiscad.splitties.eap.bottomsheet
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ModalBottomSheetValue
@ExperimentalMaterialApi
val ModalBottomSheetState.isAtLeastPartiallyVisible: Boolean
get() = progress.from != ModalBottomSheetValue.Hidden ||
progress.to != ModalBottomSheetValue.Hidden
@ExperimentalMaterialApi
val ModalBottomSheetState.isShownOrShowing: Boolean
get() = targetValue != ModalBottomSheetValue.Hidden
@ExperimentalMaterialApi
val ModalBottomSheetState.isHiddenOrHiding: Boolean
get() = targetValue == ModalBottomSheetValue.Hidden
package com.louiscad.splitties.eap.bottomsheet
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun rememberModalState(): ModalState {
val bottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
return remember(bottomSheetState) { ModalStateImpl(bottomSheetState) }
}
@Stable
sealed interface ModalState {
val isVisible: Boolean
val isAtLeastPartiallyVisible: Boolean
val isShownOrShowing: Boolean
val isHiddenOrHiding: Boolean
suspend fun hide()
suspend fun show()
}
@Stable
@ExperimentalMaterialApi
internal class ModalStateImpl(
val _state: ModalBottomSheetState
) : ModalState {
override val isVisible: Boolean get() = _state.isVisible
override val isAtLeastPartiallyVisible: Boolean get() = _state.isAtLeastPartiallyVisible
override val isShownOrShowing: Boolean get() = _state.isShownOrShowing
override val isHiddenOrHiding: Boolean get() = _state.isHiddenOrHiding
override suspend fun hide() = _state.hide()
override suspend fun show() {
try {
showRequestsPendingCount++
_state.show()
} finally {
showRequestsPendingCount--
}
}
override fun toString(): String = buildString {
appendLine("isVisible: " + _state.isVisible.toString())
appendLine("currentValue: " + _state.currentValue.toString())
appendLine("targetValue: " + _state.targetValue.toString())
appendLine("isAnimationRunning: " + _state.isAnimationRunning.toString())
}
var showRequestsPendingCount by mutableStateOf(0)
val hasPendingShowRequests by derivedStateOf { showRequestsPendingCount > 0 }
}
package com.louiscad.splitties.eap.bottomsheet.example
@Composable
fun BottomSheetExample() {
val modalState = rememberModalState()
val coroutineScope = rememberCoroutineScope()
Button(
onClick = { coroutineScope.launch { modalState.show() } }
) {
Text("Open the bottom sheet!")
}
ModalBottomSheet(modalState = modalState) {
Surface {
Text("Your content here")
Button(
onClick = { coroutineScope.launch { modalState.hide() }
) { Text("Dismiss it!") }
Spacer(Modifier.navigationBarsPadding())
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment