Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
// Copyright (C) 2022 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuitx.overlays
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.ModalBottomSheetDefaults
import androidx.compose.material3.ModalBottomSheetProperties
import androidx.compose.material3.SheetValue
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
import com.slack.circuit.foundation.internal.BackHandler
import com.slack.circuit.overlay.Overlay
import com.slack.circuit.overlay.OverlayNavigator
import com.slack.circuit.runtime.internal.rememberStableCoroutineScope
import kotlinx.coroutines.launch
/**
* An [Overlay] that shows a [ModalBottomSheet] with the given [model] and [content]. When the sheet
* is dismissed, it emits a [Result] type to the [OverlayNavigator]. This is useful for showing
* temporary content in a sheet that you want to return back to the calling UI, such as a picker UI
* or user input prompt.
*
* @property model The model to pass to the [content] composable. This should be thought of like a
* state model.
* @property dismissOnTapOutside Controls whether the sheet can be dismissed by tapping outside of
* it.
* @property onDismiss A callback that is invoked when the sheet is dismissed with a result. This is
* required if [dismissOnTapOutside] is true.
* @property content The Composable content to show in the sheet.
*/
@OptIn(ExperimentalMaterial3Api::class)
public class BottomSheetOverlay
private constructor(
private val model: Model,
private val dismissOnTapOutside: Boolean,
private val onDismiss: (() -> Result)?,
private val sheetShape: Shape?,
private val sheetContainerColor: Color?,
private val dragHandle: (@Composable () -> Unit)?,
private val skipPartiallyExpandedState: Boolean,
private val properties: ModalBottomSheetProperties,
private val content: @Composable (Model, OverlayNavigator) -> Unit,
) : Overlay {
/**
* Constructs a new [BottomSheetOverlay] that will not dismiss when tapped outside of the sheet.
* This means that only the [content] can finish the overlay. Additionally the appearance of the
* sheet can be customized.
*
* @param sheetContainerColor set the container color of the ModalBottomSheet
* @param dragHandle customize the drag handle of the sheet
* @param skipPartiallyExpandedState indicates if the Sheet should be expanded per default (if
* it's height exceed the partial height threshold)
* @param isFocusable corresponds to [ModalBottomSheetProperties.isFocusable] and will be passed
* on to the final sheet as such.
*/
public constructor(
model: Model,
sheetContainerColor: Color? = null,
sheetShape: Shape? = null,
dragHandle: @Composable (() -> Unit)? = null,
skipPartiallyExpandedState: Boolean = false,
isFocusable: Boolean = true,
content: @Composable (Model, OverlayNavigator) -> Unit,
) : this(
model = model,
dismissOnTapOutside = false,
onDismiss = null,
dragHandle = dragHandle,
sheetShape = sheetShape,
sheetContainerColor = sheetContainerColor,
skipPartiallyExpandedState = skipPartiallyExpandedState,
properties =
createBottomSheetProperties(isFocusable = isFocusable, shouldDismissOnBackPress = false),
content = content,
)
/**
* Constructs a new [BottomSheetOverlay] that will dismiss when tapped outside of the sheet.
* [onDismiss] is required in this case to offer a default value in this event. Additionally the
* appearance of the sheet can be customized.
*
* @param sheetContainerColor set the container color of the ModalBottomSheet
* @param dragHandle customize the drag handle of the sheet
* @param skipPartiallyExpandedState indicates if the Sheet should be expanded per default (if
* it's height exceed the partial height threshold)
* @param properties any [ModalBottomSheetProperties]. Defaults to
* [ModalBottomSheetDefaults.properties].
*/
public constructor(
model: Model,
onDismiss: (() -> Result),
sheetContainerColor: Color? = null,
sheetShape: Shape? = null,
dragHandle: @Composable (() -> Unit)? = null,
skipPartiallyExpandedState: Boolean = false,
properties: ModalBottomSheetProperties = DEFAULT_PROPERTIES,
content: @Composable (Model, OverlayNavigator) -> Unit,
) : this(
model = model,
dismissOnTapOutside = true,
onDismiss = onDismiss,
dragHandle = dragHandle,
sheetShape = sheetShape,
sheetContainerColor = sheetContainerColor,
skipPartiallyExpandedState = skipPartiallyExpandedState,
properties = properties,
content = content,
)
@Composable
override fun Content(navigator: OverlayNavigator) {
var hasShown by remember { mutableStateOf(false) }
val sheetState =
rememberModalBottomSheetState(
skipPartiallyExpanded = skipPartiallyExpandedState,
confirmValueChange = { newValue ->
if (hasShown && newValue == SheetValue.Hidden) {
dismissOnTapOutside
} else {
true
}
},
)
var pendingResult by remember { mutableStateOf(null) }
ModalBottomSheet(
content = {
val coroutineScope = rememberStableCoroutineScope()
BackHandler(enabled = sheetState.isVisible) {
coroutineScope
.launch { sheetState.hide() }
.invokeOnCompletion {
if (!sheetState.isVisible) {
navigator.finish(onDismiss!!.invoke())
}
}
}
// Delay setting the result until we've finished dismissing
content(model) { result ->
// This is the OverlayNavigator.finish() callback
coroutineScope.launch {
pendingResult = result
sheetState.hide()
}
}
},
sheetState = sheetState,
shape = sheetShape ?: RoundedCornerShape(32.dp),
containerColor = sheetContainerColor ?: BottomSheetDefaults.ContainerColor,
dragHandle = dragHandle ?: { BottomSheetDefaults.DragHandle() },
// Go edge-to-edge
windowInsets = WindowInsets(0, 0, 0, 0),
onDismissRequest = {
// Only possible if dismissOnTapOutside is false
check(dismissOnTapOutside)
navigator.finish(onDismiss!!.invoke())
},
properties = properties,
)
LaunchedEffect(model, onDismiss) {
snapshotFlow { sheetState.currentValue }
.collect { newValue ->
if (hasShown && newValue == SheetValue.Hidden) {
// This is apparently as close as we can get to an "onDismiss" callback, which
// unfortunately has no animation
val result = pendingResult ?: onDismiss?.invoke() ?: error("no result!")
navigator.finish(result)
}
}
}
LaunchedEffect(model, onDismiss) {
hasShown = true
sheetState.show()
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
internal val DEFAULT_PROPERTIES: ModalBottomSheetProperties = ModalBottomSheetDefaults.properties()
@OptIn(ExperimentalMaterial3Api::class)
internal expect fun createBottomSheetProperties(
isFocusable: Boolean = DEFAULT_PROPERTIES.isFocusable,
shouldDismissOnBackPress: Boolean = DEFAULT_PROPERTIES.shouldDismissOnBackPress,
): ModalBottomSheetProperties