Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-18 16:55:47 +04:00
parent 00c5775f03
commit 5d4fd522ac
12 changed files with 1038 additions and 60 deletions

View file

@ -4,8 +4,9 @@ import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.material3.SheetValue.Expanded
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -31,6 +32,9 @@ import com.tangem.core.ui.components.bottomsheets.internal.collapse
import com.tangem.core.ui.components.bottomsheets.modal.MODAL_SHEET_MAX_HEIGHT
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetDraggableHeader
import com.tangem.core.ui.components.sheetscaffold.TangemSheetState
import com.tangem.core.ui.components.sheetscaffold.TangemSheetValue
import com.tangem.core.ui.components.sheetscaffold.rememberSheetState
import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
@ -131,14 +135,14 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultModalBottomSheetW
var isVisible by remember { mutableStateOf(value = config.isShown) }
val sheetState = if (config.dismissOnClickOutside == null) {
rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
rememberSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
} else {
rememberModalBottomSheetState(
rememberSheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
confirmValueChange = { sheetValue ->
if (config.dismissOnClickOutside().not()) {
// Ignore transitions to hidden (prevents dismiss on outside click/back press)
sheetValue != SheetValue.Hidden
sheetValue != TangemSheetValue.Hidden
} else {
true
}
@ -182,11 +186,9 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheetW
) {
BasicBottomSheet<T>(
config = config,
sheetState = SheetState(
sheetState = rememberSheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
initialValue = Expanded,
positionalThreshold = { 0f },
velocityThreshold = { 0f },
initialValue = TangemSheetValue.Expanded,
),
onBack = null,
containerColor = containerColor,
@ -197,12 +199,12 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheetW
)
}
@Suppress("LongParameterList", "LongMethod")
@Suppress("LongParameterList", "LongMethod", "ComposableParametersOrdering")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
config: TangemBottomSheetConfig,
sheetState: SheetState,
sheetState: TangemSheetState = rememberSheetState(),
containerColor: Color,
type: TangemBottomSheetType,
modifier: Modifier = Modifier,
@ -216,13 +218,12 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }
var footerHeightDp by remember { mutableStateOf<Dp?>(null) }
val maxHeight = when (type) {
Default -> Dp.Unspecified
Modal -> windowSize.height * MODAL_SHEET_MAX_HEIGHT
}
val bsContent: @Composable ColumnScope.() -> Unit = {
val maxHeight = when (type) {
Default -> Dp.Unspecified
Modal -> windowSize.height * MODAL_SHEET_MAX_HEIGHT
}
val contentModifier = when (type) {
Default -> Modifier
.clip(
@ -276,6 +277,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
onBack = onBack,
dragHandle = type.getDragHandle(),
content = bsContent,
peekHeightDp = maxHeight,
scrimColor = TangemTheme.colors2.overlay.overlaySecondary,
)
}

View file

@ -0,0 +1,358 @@
/*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tangem.core.ui.components.bottomsheets.copy
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetValue.Hidden
import androidx.compose.material3.Surface
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.*
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import com.tangem.core.ui.components.bottomsheets.copy.internal.DragHandleWithTooltip
import com.tangem.core.ui.components.bottomsheets.copy.internal.ModalBottomSheetDialog
import com.tangem.core.ui.components.bottomsheets.copy.internal.ModalBottomSheetProperties
import com.tangem.core.ui.components.bottomsheets.copy.internal.StandardMotionTokens
import com.tangem.core.ui.components.sheetscaffold.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.math.min
/**
* [Material Design modal bottom sheet](https://m3.material.io/components/bottom-sheets/overview)
*
* Modal bottom sheets are used as an alternative to inline menus or simple dialogs on mobile,
* especially when offering a long list of action items, or when items require longer descriptions
* and icons. Like dialogs, modal bottom sheets appear in front of app content, disabling all other
* app functionality when they appear, and remaining on screen until confirmed, dismissed, or a
* required action has been taken.
*
* ![Bottom sheet
* image](https://developer.android.com/images/reference/androidx/compose/material3/bottom_sheet.png)
*
* A simple example of a modal bottom sheet looks like this:
*
* @sample androidx.compose.material3.samples.ModalBottomSheetSample
* @param onDismissRequest Executes when the user clicks outside of the bottom sheet, after sheet
* animates to [Hidden].
* @param modifier Optional [Modifier] for the bottom sheet.
* @param sheetState The state of the bottom sheet.
* @param sheetMaxWidth [Dp] that defines what the maximum width the sheet will take. Pass in
* [Dp.Unspecified] for a sheet that spans the entire screen width.
* @param sheetGesturesEnabled Whether the bottom sheet can be interacted with by gestures.
* @param shape The shape of the bottom sheet.
* @param containerColor The color used for the background of this bottom sheet
* @param contentColor The preferred color for content inside this bottom sheet. Defaults to either
* the matching content color for [containerColor], or to the current [LocalContentColor] if
* [containerColor] is not a color from the theme.
* @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
* overlay is applied on top of the container. A higher tonal elevation value will result in a
* darker color in light theme and lighter color in dark theme. See also: [Surface].
* @param scrimColor Color of the scrim that obscures content when the bottom sheet is open.
* @param dragHandle Optional visual marker to swipe the bottom sheet.
* @param contentWindowInsets callback which provides window insets to be passed to the bottom sheet
* content via [Modifier.windowInsetsPadding]. [ModalBottomSheet] will pre-emptively consume top
* insets based on it's current offset. This keeps content outside of the expected window insets
* at any position.
* @param properties [ModalBottomSheetProperties] for further customization of this modal bottom
* sheet's window behavior.
* @param content The content to be displayed inside the bottom sheet.
*/
@Composable
@ExperimentalMaterial3Api
@Suppress(
"LongParameterList",
"LongMethod",
"MagicNumber",
"ComposableEventParameterNaming",
"ComposableParametersOrdering",
"ReusedModifierInstance",
)
fun ModalBottomSheet(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
sheetState: TangemSheetState = rememberTangemStandardBottomSheetState(),
sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
sheetGesturesEnabled: Boolean = true,
shape: Shape = BottomSheetDefaults.ExpandedShape,
containerColor: Color = BottomSheetDefaults.ContainerColor,
contentColor: Color = contentColorFor(containerColor),
tonalElevation: Dp = 0.dp,
peekHeightDp: Dp,
scrimColor: Color = BottomSheetDefaults.ScrimColor,
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets },
properties: ModalBottomSheetProperties = ModalBottomSheetProperties(),
content: @Composable ColumnScope.() -> Unit,
) {
val scope = rememberCoroutineScope()
val animateToDismiss: () -> Unit = {
scope
.launch { sheetState.hide() }
.invokeOnCompletion {
if (!sheetState.isVisible) {
onDismissRequest()
}
}
}
val settleToDismiss: (velocity: Float) -> Unit = {
scope
.launch { sheetState.settle(it) }
.invokeOnCompletion { if (!sheetState.isVisible) onDismissRequest() }
}
val predictiveBackProgress = remember { Animatable(initialValue = 0f) }
ModalBottomSheetDialog(
properties = properties,
contentColor = contentColor,
onDismissRequest = {
if (sheetState.currentValue == TangemSheetValue.Expanded && sheetState.hasPartiallyExpandedState) {
// Smoothly animate away predictive back transformations since we are not fully
// dismissing. We don't need to do this in the else below because we want to
// preserve the predictive back transformations (scale) during the hide animation.
scope.launch { predictiveBackProgress.animateTo(0f) }
scope.launch { sheetState.partialExpand() }
} else { // Is expanded without collapsed state or is collapsed.
scope.launch { sheetState.hide() }.invokeOnCompletion { onDismissRequest() }
}
},
predictiveBackProgress = predictiveBackProgress,
) {
Box(
modifier = Modifier
.fillMaxSize()
.imePadding()
.semantics { isTraversalGroup = true },
) {
Scrim(
color = scrimColor,
onDismissRequest = animateToDismiss,
visible = sheetState.targetValue != TangemSheetValue.Hidden,
dismissEnabled = properties.shouldDismissOnClickOutside,
)
ModalBottomSheetContent(
predictiveBackProgress = predictiveBackProgress,
scope = scope,
animateToDismiss = animateToDismiss,
settleToDismiss = settleToDismiss,
modifier = modifier,
sheetState = sheetState,
sheetMaxWidth = sheetMaxWidth,
sheetGesturesEnabled = sheetGesturesEnabled,
shape = shape,
containerColor = containerColor,
contentColor = contentColor,
tonalElevation = tonalElevation,
peekHeightDp = peekHeightDp,
dragHandle = dragHandle,
contentWindowInsets = contentWindowInsets,
content = content,
)
}
}
if (sheetState.hasExpandedState) {
LaunchedEffect(sheetState) { sheetState.show() }
}
}
@Composable
@ExperimentalMaterial3Api
@Suppress(
"LongParameterList",
"LongMethod",
"MagicNumber",
"ComposableEventParameterNaming",
"ComposableParametersOrdering",
)
internal fun BoxScope.ModalBottomSheetContent(
predictiveBackProgress: Animatable<Float, AnimationVector1D>,
scope: CoroutineScope,
animateToDismiss: () -> Unit,
settleToDismiss: (velocity: Float) -> Unit,
modifier: Modifier = Modifier,
sheetState: TangemSheetState = rememberTangemStandardBottomSheetState(),
sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
sheetGesturesEnabled: Boolean = true,
shape: Shape = BottomSheetDefaults.ExpandedShape,
containerColor: Color = BottomSheetDefaults.ContainerColor,
contentColor: Color = contentColorFor(containerColor),
tonalElevation: Dp = BottomSheetDefaults.Elevation,
peekHeightDp: Dp,
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets },
content: @Composable ColumnScope.() -> Unit,
) {
val orientation = Orientation.Vertical
val peekHeightPx = with(LocalDensity.current) { peekHeightDp.toPx() }
Surface(
modifier =
modifier
.align(Alignment.TopCenter)
.widthIn(max = sheetMaxWidth)
.fillMaxWidth()
.then(
if (sheetGesturesEnabled) {
Modifier.nestedScroll(
remember(sheetState) {
consumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
sheetState = sheetState,
orientation = Orientation.Vertical,
onFling = settleToDismiss,
)
},
)
} else {
Modifier
},
)
.bottomSheetDraggableAnchor(sheetState, Orientation.Vertical, peekHeightPx)
.anchoredDraggable(
state = sheetState.anchoredDraggableState,
orientation = orientation,
enabled = sheetGesturesEnabled,
)
.consumeWindowInsets(WindowInsets(top = sheetState.offset.toInt().coerceAtLeast(0)))
.graphicsLayer {
val sheetOffset = sheetState.anchoredDraggableState.offset
val sheetHeight = size.height
if (!sheetOffset.isNaN() && !sheetHeight.isNaN() && sheetHeight != 0f) {
val progress = predictiveBackProgress.value
scaleX = calculatePredictiveBackScaleX(progress)
scaleY = calculatePredictiveBackScaleY(progress)
@Suppress("MagicNumber")
transformOrigin =
TransformOrigin(0.5f, (sheetOffset + sheetHeight) / sheetHeight)
}
},
shape = shape,
color = containerColor,
contentColor = contentColor,
tonalElevation = tonalElevation,
) {
Column(
Modifier
.fillMaxWidth()
.windowInsetsPadding(contentWindowInsets())
.graphicsLayer {
val progress = predictiveBackProgress.value
val predictiveBackScaleX = calculatePredictiveBackScaleX(progress)
val predictiveBackScaleY = calculatePredictiveBackScaleY(progress)
// Preserve the original aspect ratio and alignment of the child content.
scaleY =
if (predictiveBackScaleY != 0f) {
predictiveBackScaleX / predictiveBackScaleY
} else {
1f
}
transformOrigin = PredictiveBackChildTransformOrigin
},
) {
if (dragHandle != null) {
DragHandleWithTooltip {
Box(
modifier =
Modifier
.clickable {
when (sheetState.currentValue) {
TangemSheetValue.Expanded -> animateToDismiss()
TangemSheetValue.PartiallyExpanded -> scope.launch { sheetState.expand() }
else -> scope.launch { sheetState.show() }
}
},
) {
dragHandle()
}
}
}
content()
}
}
}
private fun GraphicsLayerScope.calculatePredictiveBackScaleX(progress: Float): Float {
val width = size.width
return if (width.isNaN() || width == 0f) {
1f
} else {
1f - lerp(0f, min(PredictiveBackMaxScaleXDistance.toPx(), width), progress) / width
}
}
private fun GraphicsLayerScope.calculatePredictiveBackScaleY(progress: Float): Float {
val height = size.height
return if (height.isNaN() || height == 0f) {
1f
} else {
1f - lerp(0f, min(PredictiveBackMaxScaleYDistance.toPx(), height), progress) / height
}
}
@Composable
private fun Scrim(color: Color, onDismissRequest: () -> Unit, visible: Boolean, dismissEnabled: Boolean) {
// TODO Load the motionScheme tokens from the component tokens file
if (color.isSpecified) {
val alpha by
animateFloatAsState(
targetValue = if (visible) 1f else 0f,
animationSpec = spring(
dampingRatio = StandardMotionTokens.SpringDefaultEffectsDamping,
stiffness = StandardMotionTokens.SpringDefaultEffectsStiffness,
),
)
val dismissSheet =
if (dismissEnabled) {
Modifier
.pointerInput(onDismissRequest) { detectTapGestures { onDismissRequest() } }
} else {
Modifier
}
Canvas(
Modifier
.fillMaxSize()
.then(dismissSheet),
) {
drawRect(color = color, alpha = alpha.coerceIn(0f, 1f))
}
}
}
private val PredictiveBackMaxScaleXDistance = 48.dp
private val PredictiveBackMaxScaleYDistance = 24.dp
private val PredictiveBackChildTransformOrigin = TransformOrigin(0.5f, 0f)

View file

@ -0,0 +1,515 @@
/*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tangem.core.ui.components.bottomsheets.copy.internal
import android.content.Context
import android.graphics.Outline
import android.os.Build
import android.view.*
import androidx.activity.BackEventCompat
import androidx.activity.ComponentDialog
import androidx.activity.OnBackPressedCallback
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.Easing
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.R
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.platform.*
import androidx.compose.ui.semantics.dialog
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogWindowProvider
import androidx.compose.ui.window.SecureFlagPolicy
import androidx.core.view.WindowCompat
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.findViewTreeViewModelStoreOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.util.UUID
// Logic forked from androidx.compose.ui.window.DialogProperties. Removed dismissOnClickOutside
// and usePlatformDefaultWidth as they are not relevant for fullscreen experience.
/**
* Properties used to customize the behavior of a [ModalBottomSheet].
*
* @param securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the bottom
* sheet's window.
* @param shouldDismissOnBackPress Whether the modal bottom sheet can be dismissed by pressing the
* back button. If true, pressing the back button will call onDismissRequest.
*/
@Immutable
@ExperimentalMaterial3Api
class ModalBottomSheetProperties {
val securePolicy: SecureFlagPolicy
val shouldDismissOnBackPress: Boolean
@get:JvmName("shouldDismissOnClickOutside") val shouldDismissOnClickOutside: Boolean
internal val isAppearanceLightStatusBars: Boolean?
internal val isAppearanceLightNavigationBars: Boolean?
/**
* Properties used to customize the behavior of a [ModalBottomSheet].
*
* This constructor provides default behavior for [ModalBottomSheet]. See other constructors for
* customization options.
*/
constructor() {
this.securePolicy = SecureFlagPolicy.Inherit
this.shouldDismissOnBackPress = true
this.shouldDismissOnClickOutside = true
this.isAppearanceLightStatusBars = null
this.isAppearanceLightNavigationBars = null
}
constructor(shouldDismissOnBackPress: Boolean, shouldDismissOnClickOutside: Boolean) {
this.securePolicy = SecureFlagPolicy.Inherit
this.shouldDismissOnBackPress = shouldDismissOnBackPress
this.shouldDismissOnClickOutside = shouldDismissOnClickOutside
this.isAppearanceLightNavigationBars = null
this.isAppearanceLightStatusBars = null
}
/**
* Properties used to customize the behavior of a [ModalBottomSheet].
*
* @param securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the bottom
* sheet's window.
* @param shouldDismissOnBackPress Whether the modal bottom sheet can be dismissed by pressing
* the back button. If true, pressing the back button will call onDismissRequest.
* @param shouldDismissOnClickOutside Whether the modal bottom sheet can be dismissed by
* clicking on the scrim.
*/
constructor(
securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
shouldDismissOnBackPress: Boolean = true,
shouldDismissOnClickOutside: Boolean = true,
) {
this.securePolicy = securePolicy
this.shouldDismissOnBackPress = shouldDismissOnBackPress
this.shouldDismissOnClickOutside = shouldDismissOnClickOutside
this.isAppearanceLightNavigationBars = null
this.isAppearanceLightStatusBars = null
}
/**
* Properties used to customize the behavior of a [ModalBottomSheet].
*
* Use this constructor to customize the behavior of status and navigation bars on the
* [ModalBottomSheet] window.
*
* @param isAppearanceLightStatusBars If true, changes the foreground color of the status bars
* to light so that the items on the bar can be read clearly. If false, reverts to the default
* appearance.
* @param isAppearanceLightNavigationBars If true, changes the foreground color of the
* navigation bars to light so that the items on the bar can be read clearly. If false,
* reverts to the default appearance.
* @param securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the bottom
* sheet's window.
* @param shouldDismissOnBackPress Whether the modal bottom sheet can be dismissed by pressing
* the back button. If true, pressing the back button will call onDismissRequest.
* @param shouldDismissOnClickOutside Whether the modal bottom sheet can be dismissed by
* clicking on the scrim.
*/
constructor(
isAppearanceLightStatusBars: Boolean,
isAppearanceLightNavigationBars: Boolean,
securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
shouldDismissOnBackPress: Boolean = true,
shouldDismissOnClickOutside: Boolean = true,
) {
this.shouldDismissOnBackPress = shouldDismissOnBackPress
this.shouldDismissOnClickOutside = shouldDismissOnClickOutside
this.securePolicy = securePolicy
this.isAppearanceLightStatusBars = isAppearanceLightStatusBars
this.isAppearanceLightNavigationBars = isAppearanceLightNavigationBars
}
@Deprecated(
message = "Use empty constructor or constructor including shouldDismissOnScrimClick param.",
level = DeprecationLevel.HIDDEN,
)
constructor(
securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
shouldDismissOnBackPress: Boolean = true,
) : this(securePolicy, shouldDismissOnBackPress, true)
@Deprecated(
message = "Use empty constructor or constructor including shouldDismissOnScrimClick param.",
level = DeprecationLevel.HIDDEN,
)
constructor(
isAppearanceLightStatusBars: Boolean,
isAppearanceLightNavigationBars: Boolean,
securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
shouldDismissOnBackPress: Boolean = true,
) {
this.shouldDismissOnBackPress = shouldDismissOnBackPress
this.shouldDismissOnClickOutside = true
this.securePolicy = securePolicy
this.isAppearanceLightStatusBars = isAppearanceLightStatusBars
this.isAppearanceLightNavigationBars = isAppearanceLightNavigationBars
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ModalBottomSheetProperties) return false
if (securePolicy != other.securePolicy) return false
if (isAppearanceLightStatusBars != other.isAppearanceLightStatusBars) return false
if (isAppearanceLightNavigationBars != other.isAppearanceLightNavigationBars) return false
if (shouldDismissOnClickOutside != other.shouldDismissOnClickOutside) return false
if (shouldDismissOnBackPress != other.shouldDismissOnBackPress) return false
return true
}
override fun hashCode(): Int {
var result = securePolicy.hashCode()
result = 31 * result + shouldDismissOnBackPress.hashCode()
result = 31 * result + (isAppearanceLightStatusBars?.hashCode() ?: 0)
result = 31 * result + (isAppearanceLightNavigationBars?.hashCode() ?: 0)
result = 31 * result + shouldDismissOnClickOutside.hashCode()
return result
}
}
// Fork of androidx.compose.ui.window.AndroidDialog_androidKt.Dialog
// Added predictiveBackProgress param to pass into BottomSheetDialogWrapper.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun ModalBottomSheetDialog(
onDismissRequest: () -> Unit,
contentColor: Color,
properties: ModalBottomSheetProperties,
predictiveBackProgress: Animatable<Float, AnimationVector1D>,
content: @Composable () -> Unit,
) {
val view = LocalView.current
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val composition = rememberCompositionContext()
val currentContent by rememberUpdatedState(content)
val dialogId = rememberSaveable { UUID.randomUUID() }
val scope = rememberCoroutineScope()
val dialog =
remember(view, density) {
ModalBottomSheetDialogWrapper(
onDismissRequest = onDismissRequest,
properties = properties,
contentColor = contentColor,
composeView = view,
layoutDirection = layoutDirection,
density = density,
dialogId = dialogId,
predictiveBackProgress = predictiveBackProgress,
scope = scope,
)
.apply {
setContent(composition) {
Box(Modifier.semantics { dialog() }) { currentContent() }
}
}
}
DisposableEffect(dialog) {
dialog.show()
onDispose {
dialog.dismiss()
dialog.disposeComposition()
}
}
SideEffect {
dialog.updateParameters(
onDismissRequest = onDismissRequest,
properties = properties,
contentColor = contentColor,
layoutDirection = layoutDirection,
)
}
}
// Fork of androidx.compose.ui.window.DialogLayout
// Additional parameters required for current predictive back implementation.
@Suppress("ViewConstructor")
private class ModalBottomSheetDialogLayout(context: Context, override val window: Window) :
AbstractComposeView(context), DialogWindowProvider {
private var content: @Composable () -> Unit by mutableStateOf({})
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
private set
fun setContent(parent: CompositionContext, content: @Composable () -> Unit) {
setParentCompositionContext(parent)
this.content = content
shouldCreateCompositionOnAttachedToWindow = true
createComposition()
}
// Display width and height logic removed, size will always span fillMaxSize().
@Composable
override fun Content() {
content()
}
}
// Fork of androidx.compose.ui.window.DialogWrapper.
// predictiveBackProgress and scope params added for predictive back implementation.
// EdgeToEdgeFloatingDialogWindowTheme provided to allow theme to extend into status bar.
@ExperimentalMaterial3Api
@Suppress("LongParameterList", "NamedArguments")
private class ModalBottomSheetDialogWrapper(
private var onDismissRequest: () -> Unit,
private var properties: ModalBottomSheetProperties,
private var contentColor: Color,
private val composeView: View,
layoutDirection: LayoutDirection,
density: Density,
dialogId: UUID,
predictiveBackProgress: Animatable<Float, AnimationVector1D>,
scope: CoroutineScope,
) :
ComponentDialog(
ContextThemeWrapper(
composeView.context,
androidx.compose.material3.R.style.EdgeToEdgeFloatingDialogWindowTheme,
),
),
ViewRootForInspector {
private val dialogLayout: ModalBottomSheetDialogLayout
// On systems older than Android S, there is a bug in the surface insets matrix math used by
// elevation, so high values of maxSupportedElevation break accessibility services: b/232788477.
private val maxSupportedElevation = 8.dp
override val subCompositionView: AbstractComposeView
get() = dialogLayout
init {
val window = window ?: error("Dialog has no window")
window.requestFeature(Window.FEATURE_NO_TITLE)
window.setBackgroundDrawableResource(android.R.color.transparent)
WindowCompat.setDecorFitsSystemWindows(window, false)
dialogLayout =
ModalBottomSheetDialogLayout(context, window).apply {
// Set unique id for AbstractComposeView. This allows state restoration for the
// state defined inside the Dialog via rememberSaveable()
setTag(R.id.compose_view_saveable_id_tag, "Dialog:$dialogId")
// Enable children to draw their shadow by not clipping them
clipChildren = false
// Allocate space for elevation
with(density) { elevation = maxSupportedElevation.toPx() }
// Simple outline to force window manager to allocate space for shadow.
// Note that the outline affects clickable area for the dismiss listener. In
// case of shapes like circle the area for dismiss might be to small
// (rectangular outline consuming clicks outside of the circle).
outlineProvider =
object : ViewOutlineProvider() {
override fun getOutline(view: View, result: Outline) {
result.setRect(0, 0, view.width, view.height)
// We set alpha to 0 to hide the view's shadow and let the
// composable to draw its own shadow. This still enables us to get
// the extra space needed in the surface.
result.alpha = 0f
}
}
}
// Clipping logic removed because we are spanning edge to edge.
setContentView(dialogLayout)
dialogLayout.setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
dialogLayout.setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
dialogLayout.setViewTreeSavedStateRegistryOwner(
composeView.findViewTreeSavedStateRegistryOwner(),
)
// Initial setup
updateParameters(onDismissRequest, properties, contentColor, layoutDirection)
WindowCompat.getInsetsController(window, window.decorView).apply {
// Theme system bars based on content color. Light system bars provide dark icons
// and vice-versa. This maintains visible system bars for the bottom sheet window.
isAppearanceLightStatusBars =
properties.isAppearanceLightStatusBars ?: contentColor.isDark()
isAppearanceLightNavigationBars =
properties.isAppearanceLightNavigationBars ?: contentColor.isDark()
}
// Due to how the onDismissRequest callback works
// (it enforces a just-in-time decision on whether to update the state to hide the dialog)
// we need to provide a custom onBackPressedCallback to provide predictive back animations
// for this component while handling onDismissRequest.
onBackPressedDispatcher.addCallback(
owner = this,
onBackPressedCallback =
PredictiveBackOnBackPressedCallback(
isEnabled = properties.shouldDismissOnBackPress,
scope = scope,
predictiveBackProgress = predictiveBackProgress,
onDismissRequest = {
this.onDismissRequest()
}, // Ensure lambda captures current onDismissRequest
),
)
}
private fun setLayoutDirection(layoutDirection: LayoutDirection) {
dialogLayout.layoutDirection =
when (layoutDirection) {
LayoutDirection.Ltr -> android.util.LayoutDirection.LTR
LayoutDirection.Rtl -> android.util.LayoutDirection.RTL
}
}
fun setContent(parentComposition: CompositionContext, children: @Composable () -> Unit) {
dialogLayout.setContent(parentComposition, children)
}
@Suppress("BooleanPropertyNaming", "UnsafeCallOnNullableType")
private fun setSecurePolicy(securePolicy: SecureFlagPolicy) {
val secureFlagEnabled =
securePolicy.shouldApplySecureFlag(composeView.isFlagSecureEnabled())
window!!.setFlags(
if (secureFlagEnabled) {
WindowManager.LayoutParams.FLAG_SECURE
} else {
WindowManager.LayoutParams.FLAG_SECURE.inv()
},
WindowManager.LayoutParams.FLAG_SECURE,
)
}
@Suppress("MagicNumber")
fun updateParameters(
onDismissRequest: () -> Unit,
properties: ModalBottomSheetProperties,
contentColor: Color,
layoutDirection: LayoutDirection,
) {
this.onDismissRequest = onDismissRequest
this.properties = properties
this.contentColor = contentColor
setSecurePolicy(properties.securePolicy)
setLayoutDirection(layoutDirection)
// Window flags to span parent window.
window?.setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT,
)
window?.setSoftInputMode(
if (Build.VERSION.SDK_INT >= 30) {
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
} else {
@Suppress("DEPRECATION") WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
},
)
}
fun disposeComposition() {
dialogLayout.disposeComposition()
}
@Suppress("BooleanPropertyNaming")
override fun onTouchEvent(event: MotionEvent): Boolean {
val result = super.onTouchEvent(event)
if (result) {
onDismissRequest()
}
return result
}
override fun cancel() {
// Prevents the dialog from dismissing itself
return
}
private class PredictiveBackOnBackPressedCallback(
isEnabled: Boolean,
val scope: CoroutineScope,
val predictiveBackProgress: Animatable<Float, AnimationVector1D>,
var onDismissRequest: () -> Unit,
) : OnBackPressedCallback(isEnabled) {
override fun handleOnBackStarted(backEvent: BackEventCompat) {
scope.launch {
predictiveBackProgress.snapTo(PredictiveBack.transform(backEvent.progress))
}
}
override fun handleOnBackProgressed(backEvent: BackEventCompat) {
scope.launch {
// Use snapTo for immediate feedback during the gesture
predictiveBackProgress.snapTo(PredictiveBack.transform(backEvent.progress))
}
}
override fun handleOnBackPressed() {
// Back gesture completed successfully, invoke dismiss
onDismissRequest()
}
override fun handleOnBackCancelled() {
// Back gesture cancelled, animate back to 0
scope.launch { predictiveBackProgress.animateTo(0f) }
}
}
}
internal fun View.isFlagSecureEnabled(): Boolean {
val windowParams = rootView.layoutParams as? WindowManager.LayoutParams
if (windowParams != null) {
return windowParams.flags and WindowManager.LayoutParams.FLAG_SECURE != 0
}
return false
}
/** Determines if a color should be considered light or dark. */
@Suppress("MagicNumber")
internal fun Color.isDark(): Boolean {
return this != Color.Transparent && luminance() <= 0.5
}
private val PredictiveBackEasing: Easing = CubicBezierEasing(a = 0.1f, b = 0.1f, c = 0f, d = 1f)
internal object PredictiveBack {
internal fun transform(progress: Float) = PredictiveBackEasing.transform(progress)
}
// Taken from AndroidPopup.android.kt
internal fun SecureFlagPolicy.shouldApplySecureFlag(isSecureFlagSetOnParent: Boolean): Boolean {
return when (this) {
SecureFlagPolicy.SecureOff -> false
SecureFlagPolicy.SecureOn -> true
SecureFlagPolicy.Inherit -> isSecureFlagSetOnParent
}
}

View file

@ -0,0 +1,46 @@
/*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tangem.core.ui.components.bottomsheets.copy.internal
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Text
import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun ColumnScope.DragHandleWithTooltip(content: @Composable (() -> Unit)) {
val dragHandleDescription = ""
// We need outer box for alignment because TooltipBox's modifier is only applied to its anchor.
Box(Modifier.align(CenterHorizontally)) {
TooltipBox(
positionProvider =
TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
tooltip = { PlainTooltip { Text(dragHandleDescription) } },
state = rememberTooltipState(),
content = content,
)
}
}

View file

@ -0,0 +1,22 @@
/*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// VERSION: v0_14_0
// GENERATED CODE - DO NOT MODIFY BY HAND
package com.tangem.core.ui.components.bottomsheets.copy.internal
internal object StandardMotionTokens {
const val SpringDefaultEffectsDamping = 1.0f
const val SpringDefaultEffectsStiffness = 1600.0f
}

View file

@ -11,8 +11,13 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.bottomsheets.copy.ModalBottomSheet
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.components.sheetscaffold.TangemSheetState
import com.tangem.core.ui.components.sheetscaffold.TangemSheetValue
import com.tangem.core.ui.components.sheetscaffold.rememberSheetState
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHost
import com.tangem.core.ui.components.bottomsheets.copy.internal.ModalBottomSheetProperties
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.LocalTopSnackbarHostState
import com.tangem.core.ui.res.TangemTheme
@ -22,11 +27,13 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Suppress("LongParameterList", "LongMethod", "ComposableParametersOrdering")
fun InternalBottomSheet(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
onBack: (() -> Unit)? = null,
sheetState: SheetState = rememberModalBottomSheetState(),
sheetState: TangemSheetState = rememberSheetState(),
peekHeightDp: Dp,
sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
shape: Shape = BottomSheetDefaults.ExpandedShape,
containerColor: Color = BottomSheetDefaults.ContainerColor,
@ -54,6 +61,7 @@ fun InternalBottomSheet(
properties = ModalBottomSheetProperties(
shouldDismissOnBackPress = onBack == null,
),
peekHeightDp = peekHeightDp,
content = {
Box {
val hazeState = rememberHazeState()
@ -73,7 +81,7 @@ fun InternalBottomSheet(
}
}
BackHandler(enabled = onBack != null && sheetState.targetValue != SheetValue.Hidden) {
BackHandler(enabled = onBack != null && sheetState.targetValue != TangemSheetValue.Hidden) {
onBack?.invoke()
}
},
@ -81,7 +89,7 @@ fun InternalBottomSheet(
}
@OptIn(ExperimentalMaterial3Api::class)
suspend fun SheetState.collapse(onCollapsed: () -> Unit) {
suspend fun TangemSheetState.collapse(onCollapsed: () -> Unit) {
coroutineScope {
launch { hide() }.invokeOnCompletion { onCollapsed() }
}

View file

@ -3,21 +3,30 @@ package com.tangem.core.ui.components.bottomsheets.internal
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.material3.*
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import com.tangem.core.ui.components.bottomsheets.copy.internal.ModalBottomSheetProperties
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.bottomsheets.copy.ModalBottomSheet
import com.tangem.core.ui.components.sheetscaffold.TangemSheetState
import com.tangem.core.ui.components.sheetscaffold.TangemSheetValue
import com.tangem.core.ui.components.sheetscaffold.rememberSheetState
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Suppress("LongParameterList", "LongMethod", "ComposableParametersOrdering")
fun ModalBottomSheetWithBackHandling(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
onBack: (() -> Unit)? = null,
sheetState: SheetState = rememberModalBottomSheetState(),
sheetState: TangemSheetState = rememberSheetState(),
peekHeightDp: Dp,
sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
shape: Shape = BottomSheetDefaults.ExpandedShape,
containerColor: Color = BottomSheetDefaults.ContainerColor,
@ -40,12 +49,13 @@ fun ModalBottomSheetWithBackHandling(
scrimColor = scrimColor,
dragHandle = dragHandle,
contentWindowInsets = contentWindowInsets,
peekHeightDp = peekHeightDp,
properties = ModalBottomSheetProperties(
shouldDismissOnBackPress = onBack == null,
),
content = {
content()
BackHandler(enabled = onBack != null && sheetState.targetValue != SheetValue.Hidden) {
BackHandler(enabled = onBack != null && sheetState.targetValue != TangemSheetValue.Hidden) {
onBack?.invoke()
}
},

View file

@ -6,8 +6,9 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.material3.SheetValue.Expanded
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -23,6 +24,7 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
@ -34,10 +36,10 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.internal.ModalBottomSheetWithBackHandling
import com.tangem.core.ui.components.bottomsheets.internal.collapse
import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
import com.tangem.core.ui.res.LocalCanScrollBackward
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.components.sheetscaffold.TangemSheetState
import com.tangem.core.ui.components.sheetscaffold.TangemSheetValue
import com.tangem.core.ui.components.sheetscaffold.rememberSheetState
import com.tangem.core.ui.res.*
import com.tangem.core.ui.utils.WindowInsetsZero
const val MODAL_SHEET_MAX_HEIGHT = 0.8f
@ -98,23 +100,26 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultModalBottomSheet(
crossinline content: @Composable (ColumnScope.(T) -> Unit),
) {
var isVisible by remember { mutableStateOf(value = config.isShown) }
val sheetState = rememberModalBottomSheetState(
val sheetState = rememberSheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
confirmValueChange = { sheetValue ->
if (!dismissOnClickOutside) {
// Ignore transitions to hidden (prevents dismiss on outside click/back press)
sheetValue != SheetValue.Hidden
sheetValue != TangemSheetValue.Hidden
} else {
true
}
},
)
val windowSize = LocalWindowSize.current
val maxHeight = windowSize.height * MODAL_SHEET_MAX_HEIGHT
if (isVisible && config.content is T) {
BasicModalBottomSheet<T>(
config = config,
sheetState = sheetState,
onBack = onBack,
peekHeightDp = maxHeight,
bsContent = {
BsContent(
config = config,
@ -146,15 +151,16 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheet(
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
) {
val windowSize = LocalWindowSize.current
val maxHeight = windowSize.height * MODAL_SHEET_MAX_HEIGHT
BasicModalBottomSheet<T>(
config = config,
sheetState = SheetState(
sheetState = rememberSheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
initialValue = Expanded,
positionalThreshold = { 0f },
velocityThreshold = { 0f },
initialValue = TangemSheetValue.Expanded,
),
onBack = null,
peekHeightDp = maxHeight,
bsContent = {
BsContent(
config = config,
@ -221,7 +227,8 @@ inline fun <reified T : TangemBottomSheetConfigContent> BsContent(
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheet(
config: TangemBottomSheetConfig,
sheetState: SheetState,
sheetState: TangemSheetState,
peekHeightDp: Dp,
modifier: Modifier = Modifier,
noinline onBack: (() -> Unit)? = null,
noinline bsContent: @Composable ColumnScope.() -> Unit,
@ -236,6 +243,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheet(
onBack = onBack,
dragHandle = null,
content = bsContent,
peekHeightDp = peekHeightDp,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}

View file

@ -7,8 +7,9 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.material3.SheetValue.Expanded
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -17,7 +18,6 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -28,7 +28,11 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.internal.ModalBottomSheetWithBackHandling
import com.tangem.core.ui.components.bottomsheets.internal.collapse
import com.tangem.core.ui.components.sheetscaffold.TangemSheetState
import com.tangem.core.ui.components.sheetscaffold.TangemSheetValue
import com.tangem.core.ui.components.sheetscaffold.rememberSheetState
import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.WindowInsetsZero
@ -89,14 +93,14 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultModalBottomSheetW
var isVisible by remember { mutableStateOf(value = config.isShown) }
val sheetState = if (config.dismissOnClickOutside == null) {
rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
rememberSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
} else {
rememberModalBottomSheetState(
rememberSheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
confirmValueChange = { sheetValue ->
if (config.dismissOnClickOutside().not()) {
// Ignore transitions to hidden (prevents dismiss on outside click/back press)
sheetValue != SheetValue.Hidden
sheetValue != TangemSheetValue.Hidden
} else {
true
}
@ -137,11 +141,9 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheetW
) {
BasicModalBottomSheetWithFooter<T>(
config = config,
sheetState = SheetState(
sheetState = rememberSheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
initialValue = Expanded,
positionalThreshold = { 0f },
velocityThreshold = { 0f },
initialValue = TangemSheetValue.Expanded,
),
onBack = null,
containerColor = containerColor,
@ -156,7 +158,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheetW
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWithFooter(
config: TangemBottomSheetConfig,
sheetState: SheetState,
sheetState: TangemSheetState,
containerColor: Color,
modifier: Modifier = Modifier,
noinline onBack: (() -> Unit)? = null,
@ -166,9 +168,11 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
) {
val model = config.content as? T ?: return
val windowSize = LocalWindowSize.current
val maxHeight = windowSize.height * MODAL_SHEET_MAX_HEIGHT
val bsContent: @Composable ColumnScope.() -> Unit = {
// FIXME: Use LocalWindowSize.current
val maxHeight = LocalConfiguration.current.screenHeightDp * MODAL_SHEET_MAX_HEIGHT
val initial = 0
val scrollState = rememberScrollState(initial = initial)
@ -198,7 +202,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
.padding(horizontal = 8.dp, vertical = 8.dp)
.clip(TangemTheme.shapes.roundedCornersLarge)
.background(containerColor)
.heightIn(max = maxHeight.dp)
.heightIn(max = maxHeight)
.fillMaxWidth(),
) {
Box(modifier = Modifier.fillMaxWidth()) {
@ -259,6 +263,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
peekHeightDp = maxHeight,
)
}

View file

@ -2,19 +2,20 @@ package com.tangem.core.ui.components.bottomsheets.sheet
import androidx.compose.foundation.layout.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue.Expanded
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.internal.ModalBottomSheetWithBackHandling
import com.tangem.core.ui.components.bottomsheets.internal.collapse
import com.tangem.core.ui.components.sheetscaffold.TangemSheetState
import com.tangem.core.ui.components.sheetscaffold.TangemSheetValue
import com.tangem.core.ui.components.sheetscaffold.rememberSheetState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
import com.tangem.core.ui.res.TangemTheme
@ -94,7 +95,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultBottomSheet(
crossinline content: @Composable (ColumnScope.(T) -> Unit),
) {
var isVisible by remember { mutableStateOf(value = config.isShown) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
val sheetState = rememberSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
if (isVisible && config.content is T) {
BasicBottomSheet<T>(
@ -130,11 +131,9 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewBottomSheet(
BasicBottomSheet<T>(
modifier = Modifier.width(360.dp),
config = config,
sheetState = SheetState(
sheetState = rememberSheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
initialValue = Expanded,
positionalThreshold = { 0f },
velocityThreshold = { 0f },
initialValue = TangemSheetValue.Expanded,
),
onBack = null,
containerColor = containerColor,
@ -149,7 +148,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewBottomSheet(
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
config: TangemBottomSheetConfig,
sheetState: SheetState,
sheetState: TangemSheetState,
containerColor: Color,
addBottomInsets: Boolean,
modifier: Modifier = Modifier,
@ -192,5 +191,6 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
onBack = onBack,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
peekHeightDp = Dp.Unspecified,
)
}

View file

@ -253,7 +253,7 @@ private fun BottomSheetScaffoldLayout(
}
}
private fun Modifier.bottomSheetDraggableAnchor(
internal fun Modifier.bottomSheetDraggableAnchor(
state: TangemSheetState,
orientation: Orientation,
peekHeightPx: Float,

View file

@ -2,7 +2,10 @@
package com.tangem.core.ui.components.sheetscaffold
import androidx.compose.animation.core.*
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.exponentialDecay
import androidx.compose.animation.core.spring
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.*
import androidx.compose.runtime.Composable
@ -16,6 +19,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.sheetscaffold.TangemSheetState.Companion.Saver
import com.tangem.core.ui.components.sheetscaffold.TangemSheetValue.*
import kotlinx.coroutines.CancellationException
@ -302,7 +306,7 @@ internal fun consumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
}
@Composable
internal fun rememberSheetState(
fun rememberSheetState(
skipPartiallyExpanded: Boolean = false,
confirmValueChange: (TangemSheetValue) -> Boolean = { true },
initialValue: TangemSheetValue = Hidden,