Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-11 15:16:56 +05:00
parent 5f4dfefc2e
commit 8411911466
13 changed files with 858 additions and 304 deletions

View file

@ -0,0 +1,440 @@
package com.tangem.core.ui.components.bottomsheets
import android.content.res.Configuration
import androidx.compose.animation.core.animateDpAsState
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.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType.Default
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType.Modal
import com.tangem.core.ui.components.bottomsheets.internal.InternalBottomSheet
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.res.LocalBottomSheetAlwaysVisible
import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.utils.WindowInsetsZero
/**
* Type of [TangemBottomSheet] that defines its behavior and appearance.
* - [Default]: Standard bottom sheet with a draggable header
* - [Modal]: Modal bottom sheet without a draggable header
*/
enum class TangemBottomSheetType {
Default, Modal;
fun getDragHandle(): (@Composable (() -> Unit))? = when (this) {
Default -> {
{ TangemBottomSheetDraggableHeader() }
}
Modal -> null
}
}
/**
* Modal bottom sheet with [content] and optional [title] and [footer].
*
* @param config Configuration for the bottom sheet, including visibility and content data.
* @param type Type of the bottom sheet that defines its behavior and appearance.
* @param containerColor Background color of the bottom sheet container.
* @param skipPartiallyExpanded Whether to skip the partially expanded state when dragging the sheet.
* @param onBack Optional callback for handling back press when the sheet is visible.
* @param title Optional composable for rendering the title section of the sheet, receiving the content
* model as a parameter.
* @param content Composable for rendering the main content of the sheet, receiving the content model
* as a parameter.
* @param footer Optional composable for rendering the footer section of the sheet, receiving the content
* model as a parameter.
*
* [Show in Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8454-23749&m=dev)
*/
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
config: TangemBottomSheetConfig,
type: TangemBottomSheetType = Default,
containerColor: Color = TangemTheme.colors2.surface.level2,
skipPartiallyExpanded: Boolean = true,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable BoxScope.(T) -> Unit = {},
crossinline content: @Composable (T) -> Unit,
noinline footer: @Composable (BoxScope.(T) -> Unit)? = null,
) {
val isAlwaysVisible = LocalBottomSheetAlwaysVisible.current
if (isAlwaysVisible) {
PreviewModalBottomSheetWithFooter<T>(
config = config,
containerColor = containerColor,
type = type,
title = title,
content = content,
footer = footer,
skipPartiallyExpanded = skipPartiallyExpanded,
)
} else {
DefaultModalBottomSheetWithFooter<T>(
config = config,
containerColor = containerColor,
type = type,
title = title,
content = content,
footer = footer,
onBack = onBack,
skipPartiallyExpanded = skipPartiallyExpanded,
)
}
}
@Suppress("LongParameterList")
@Composable
@OptIn(ExperimentalMaterial3Api::class)
inline fun <reified T : TangemBottomSheetConfigContent> DefaultModalBottomSheetWithFooter(
config: TangemBottomSheetConfig,
containerColor: Color,
type: TangemBottomSheetType,
skipPartiallyExpanded: Boolean = true,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable BoxScope.(T) -> Unit,
crossinline content: @Composable (T) -> Unit,
noinline footer: @Composable (BoxScope.(T) -> Unit)?,
) {
var isVisible by remember { mutableStateOf(value = config.isShown) }
val sheetState = if (config.dismissOnClickOutside == null) {
rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
} else {
rememberModalBottomSheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
confirmValueChange = { sheetValue ->
if (config.dismissOnClickOutside().not()) {
// Ignore transitions to hidden (prevents dismiss on outside click/back press)
sheetValue != SheetValue.Hidden
} else {
true
}
},
)
}
if (isVisible && config.content is T) {
BasicBottomSheet<T>(
config = config,
sheetState = sheetState,
containerColor = containerColor,
type = type,
title = title,
onBack = onBack,
content = content,
footer = footer,
)
}
LaunchedEffect(key1 = config.isShown) {
if (config.isShown) {
isVisible = true
} else {
sheetState.collapse { isVisible = false }
}
}
}
@Suppress("LongParameterList")
@Composable
@OptIn(ExperimentalMaterial3Api::class)
inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheetWithFooter(
config: TangemBottomSheetConfig,
containerColor: Color,
type: TangemBottomSheetType,
skipPartiallyExpanded: Boolean = true,
crossinline title: @Composable BoxScope.(T) -> Unit,
crossinline content: @Composable (T) -> Unit,
noinline footer: @Composable (BoxScope.(T) -> Unit)?,
) {
BasicBottomSheet<T>(
config = config,
sheetState = SheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
initialValue = Expanded,
positionalThreshold = { 0f },
velocityThreshold = { 0f },
),
onBack = null,
containerColor = containerColor,
type = type,
title = title,
content = content,
footer = footer,
)
}
@Suppress("LongParameterList", "LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
config: TangemBottomSheetConfig,
sheetState: SheetState,
containerColor: Color,
type: TangemBottomSheetType,
modifier: Modifier = Modifier,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable BoxScope.(T) -> Unit,
crossinline content: @Composable (T) -> Unit,
noinline footer: @Composable (BoxScope.(T) -> Unit)?,
) {
val model = config.content as? T ?: return
val windowSize = LocalWindowSize.current
val bsContent: @Composable ColumnScope.() -> Unit = {
val maxHeight = when (type) {
Default -> Dp.Unspecified
Modal -> windowSize.height * MODAL_SHEET_MAX_HEIGHT
}
val buttonHeight by animateDpAsState(
if (footer != null) {
80.dp
} else {
0.dp
},
)
val contentModifier = when (type) {
Default -> Modifier.clip(
RoundedCornerShape(
topStart = TangemTheme.dimens2.x8,
topEnd = TangemTheme.dimens2.x8,
),
)
Modal -> Modifier
.padding(
start = TangemTheme.dimens2.x2,
end = TangemTheme.dimens2.x2,
bottom = TangemTheme.dimens2.x2,
)
.clip(RoundedCornerShape(TangemTheme.dimens2.x8))
}
Column(
modifier = contentModifier
.background(containerColor)
.heightIn(max = maxHeight),
) {
Box(modifier = Modifier.fillMaxWidth()) {
title(model)
}
Box(modifier = Modifier.fillMaxWidth()) {
content(model)
if (footer != null) {
BottomFade(
modifier = Modifier.align(Alignment.BottomCenter),
gradientBrush = Brush.verticalGradient(
listOf(
TangemTheme.colors2.shadow.fadeMin,
TangemTheme.colors2.shadow.fadeMax,
),
),
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(buttonHeight)
.align(Alignment.BottomCenter),
) {
if (footer != null) {
footer(model)
}
}
}
}
}
InternalBottomSheet(
modifier = modifier.statusBarsPadding(),
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = Color.Transparent,
contentWindowInsets = { WindowInsetsZero },
onBack = onBack,
dragHandle = type.getDragHandle(),
content = bsContent,
scrimColor = TangemTheme.colors2.overlay.overlaySecondary,
)
}
// region Preview
@Suppress("LongMethod")
@Composable
@Preview(showBackground = true, widthDp = 360, heightDp = 800)
@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemModalBottomSheetWithFooter_Preview(
@PreviewParameter(TangemBottomSheetPreviewProvider::class) params: TangemBottomSheetType,
) {
TangemThemePreviewRedesign {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
type = params,
title = { TangemModalBottomSheetTitle(endIconRes = R.drawable.ic_close_24, onEndClick = {}) },
content = {
Column(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier
.size(56.dp)
.clip(RoundedCornerShape(100))
.background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f))
.padding(12.dp),
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_alert_24),
),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
SpacerH24()
Text(
text = "Unsuported networks",
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH8()
Text(
text = "Tangem does not currently support a required network by React App.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerH(48.dp)
Text(
text = "Long text to show scrollable content and bottom fade." +
"\nLorem ipsum dolor sit amet, consectetur adipiscing elit. In imperdiet metus non leo " +
"ultricies pulvinar. Pellentesque sed condimentum odio. Sed venenatis ac felis non " +
"consequat. Nunc erat dolor, maximus nec mattis a, tempus at eros. Duis sit amet neque " +
"dui. Donec consectetur nisl id dui convallis, in posuere dolor eleifend. Pellentesque " +
"habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. " +
"Pellentesque consequat scelerisque justo quis tristique. Mauris laoreet venenatis " +
"pharetra. Morbi sed faucibus leo. Praesent elementum pretium posuere. Morbi et felis a " +
"turpis pellentesque rhoncus.",
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
}
},
footer = {
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
text = "Go it",
onClick = {},
)
},
)
}
}
@Suppress("LongMethod")
@Composable
@Preview(showBackground = true, widthDp = 360, heightDp = 800)
@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemModalBottomSheetWithFooter_Preview2(
@PreviewParameter(TangemBottomSheetPreviewProvider::class) params: TangemBottomSheetType,
) {
TangemThemePreviewRedesign {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
type = params,
title = { TangemModalBottomSheetTitle(endIconRes = R.drawable.ic_close_24, onEndClick = {}) },
content = {
Column(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier
.size(56.dp)
.clip(RoundedCornerShape(100))
.background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f))
.padding(12.dp),
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_alert_24),
),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
SpacerH24()
Text(
text = "Unsuported networks",
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH8()
Text(
text = "Tangem does not currently support a required network by React App.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerH(48.dp)
Text(
text = "Long text to show scrollable content and bottom fade." +
"\nLorem ipsum dolor sit amet, consectetur adipiscing elit. In imperdiet metus non leo " +
"ultricies pulvinar. Pellentesque sed condimentum odio. Sed venenatis ac felis non " +
"consequat. Nunc erat dolor, maximus nec mattis a, tempus at eros. Duis sit amet neque " +
"dui. Donec consectetur nisl id dui convallis, in posuere dolor eleifend. Pellentesque " +
"habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. " +
"Pellentesque consequat scelerisque justo quis tristique. Mauris laoreet venenatis " +
"pharetra. Morbi sed faucibus leo. Praesent elementum pretium posuere. Morbi et felis a " +
"turpis pellentesque rhoncus.",
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
}
},
)
}
}
private class TangemBottomSheetPreviewProvider : PreviewParameterProvider<TangemBottomSheetType> {
override val values: Sequence<TangemBottomSheetType>
get() = sequenceOf(
Default,
Modal,
)
}
// endregion

View file

@ -0,0 +1,88 @@
package com.tangem.core.ui.components.bottomsheets.internal
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
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.haze.hazeSourceTangem
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHost
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.LocalTopSnackbarHostState
import com.tangem.core.ui.res.TangemTheme
import dev.chrisbanes.haze.rememberHazeState
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InternalBottomSheet(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
onBack: (() -> Unit)? = null,
sheetState: SheetState = rememberModalBottomSheetState(),
sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
shape: Shape = BottomSheetDefaults.ExpandedShape,
containerColor: Color = BottomSheetDefaults.ContainerColor,
contentColor: Color = contentColorFor(containerColor),
tonalElevation: Dp = 0.dp,
scrimColor: Color = BottomSheetDefaults.ScrimColor,
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets },
content: @Composable ColumnScope.() -> Unit,
) {
val topSnackbarHostState = LocalTopSnackbarHostState.current
ModalBottomSheet(
onDismissRequest = onDismissRequest,
modifier = modifier,
sheetState = sheetState,
sheetMaxWidth = sheetMaxWidth,
shape = shape,
containerColor = containerColor,
contentColor = contentColor,
tonalElevation = tonalElevation,
scrimColor = scrimColor,
dragHandle = dragHandle,
contentWindowInsets = contentWindowInsets,
properties = ModalBottomSheetProperties(
shouldDismissOnBackPress = onBack == null,
),
content = {
Box {
val hazeState = rememberHazeState()
Column(Modifier.hazeSourceTangem(hazeState)) {
content()
}
CompositionLocalProvider(LocalHazeState provides hazeState) {
TangemTopSnackbarHost(
modifier = Modifier
.align(Alignment.TopCenter)
.padding(horizontal = TangemTheme.dimens2.x4)
.padding(top = TangemTheme.dimens2.x6),
hostState = topSnackbarHostState,
)
}
}
BackHandler(enabled = onBack != null && sheetState.targetValue != SheetValue.Hidden) {
onBack?.invoke()
}
},
)
}
@OptIn(ExperimentalMaterial3Api::class)
suspend fun SheetState.collapse(onCollapsed: () -> Unit) {
coroutineScope {
launch { hide() }.invokeOnCompletion { onCollapsed() }
}
}

View file

@ -1,29 +1,15 @@
package com.tangem.core.ui.components.bottomsheets.internal
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.key.*
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHost
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.LocalTopSnackbarHostState
import dev.chrisbanes.haze.rememberHazeState
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -42,9 +28,6 @@ fun ModalBottomSheetWithBackHandling(
contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets },
content: @Composable ColumnScope.() -> Unit,
) {
val topSnackbarHostState = LocalTopSnackbarHostState.current
val isRedesignEnabled = LocalRedesignEnabled.current
ModalBottomSheet(
onDismissRequest = onDismissRequest,
modifier = modifier,
@ -61,38 +44,10 @@ fun ModalBottomSheetWithBackHandling(
shouldDismissOnBackPress = onBack == null,
),
content = {
if (isRedesignEnabled) {
Box {
val hazeState = rememberHazeState()
Column(Modifier.hazeSourceTangem(hazeState, zIndex = -1f)) {
content()
}
CompositionLocalProvider(LocalHazeState provides hazeState) {
TangemTopSnackbarHost(
modifier = Modifier
.align(Alignment.TopCenter)
.padding(horizontal = 16.dp)
.padding(top = 24.dp),
hostState = topSnackbarHostState,
)
}
}
} else {
content()
}
content()
BackHandler(enabled = onBack != null && sheetState.targetValue != SheetValue.Hidden) {
onBack?.invoke()
}
},
)
}
@OptIn(ExperimentalMaterial3Api::class)
suspend fun SheetState.collapse(onCollapsed: () -> Unit) {
coroutineScope {
launch { hide() }.invokeOnCompletion { onCollapsed() }
}
}

View file

@ -188,7 +188,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
containerColor = containerColor,
shape = TangemTheme.shapes.bottomSheetLarge,
contentWindowInsets = { WindowInsetsZero },
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
dragHandle = { TangemBottomSheetDraggableHeaderLegacy(color = containerColor) },
onBack = onBack,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -12,7 +13,7 @@ import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.res.TangemTheme
@Composable
fun TangemBottomSheetDraggableHeader(color: Color = TangemTheme.colors.background.primary) {
fun TangemBottomSheetDraggableHeaderLegacy(color: Color = TangemTheme.colors.background.primary) {
Surface(
modifier = Modifier.height(TangemTheme.dimens.size20),
color = color,
@ -30,4 +31,21 @@ fun TangemBottomSheetDraggableHeader(color: Color = TangemTheme.colors.backgroun
),
)
}
}
@Composable
fun TangemBottomSheetDraggableHeader() {
Box(
modifier = Modifier
.height(TangemTheme.dimens2.x3)
.padding(vertical = TangemTheme.dimens2.x1)
.size(
width = TangemTheme.dimens2.x10,
height = TangemTheme.dimens2.x1,
)
.background(
color = TangemTheme.colors2.graphic.neutral.primaryInverted,
shape = RoundedCornerShape(TangemTheme.dimens2.x0_5),
),
)
}

View file

@ -2,57 +2,34 @@
package com.tangem.core.ui.components.sheetscaffold
import android.graphics.Bitmap
import android.graphics.BlurMaskFilter
import android.renderscript.Allocation
import android.renderscript.Element
import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicBlur
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.center
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.*
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMaxOfOrNull
import androidx.compose.ui.zIndex
import androidx.core.graphics.withSave
import androidx.core.graphics.withTranslation
import com.tangem.core.ui.components.sheetscaffold.TangemSheetValue.*
import com.tangem.core.ui.extensions.softLayerShadow
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.toPx
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.pow
import kotlin.math.roundToInt
/**
@ -63,25 +40,17 @@ import kotlin.math.roundToInt
* viewing and interacting with both regions. They are commonly used to keep a feature or secondary
* content visible on screen when content in main UI region is frequently scrolled or panned.
*
* ![Bottom sheet
* image](https://developer.android.com/images/reference/androidx/compose/material3/bottom_sheet.png)
* ![Bottom sheet image](https://developer.android.com/images/reference/androidx/compose/material3/bottom_sheet.png)
*
* This component provides API to put together several material components to construct your screen,
* by ensuring proper layout strategy for them and collecting necessary data so these components
* will work together correctly.
*
* @param sheetContent the content of the bottom sheet
* @param sheetPeekHeight the height of the bottom sheet when it is collapsed
* @param modifier the [Modifier] to be applied to this scaffold
* @param scaffoldState the state of the bottom sheet scaffold
* @param sheetPeekHeight the height of the bottom sheet when it is collapsed
* @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 sheetShape the shape of the bottom sheet
* @param sheetContainerColor the background color of the bottom sheet
* @param sheetSwipeEnabled whether the sheet swiping is enabled and should react to the user's
* input
* @param topBar top app bar of the screen, typically a [SmallTopAppBar]
* @param snackbarHost component to host [Snackbar]s that are pushed to be shown via
* @param topBar top app bar of the screen.
* @param snackbarHost component to host [TangemTopSnackbar]s that are pushed to be shown via
* [SnackbarHostState.showSnackbar], typically a [SnackbarHost]
* @param containerColor the color used for the background of this scaffold. Use [Color.Transparent]
* to have no color.
@ -95,18 +64,14 @@ import kotlin.math.roundToInt
*/
@Composable
fun TangemBottomSheetScaffold(
sheetContent: @Composable ColumnScope.() -> Unit,
sheetPeekHeight: Dp,
modifier: Modifier = Modifier,
scaffoldState: TangemBottomSheetScaffoldState = rememberTangemBottomSheetScaffoldState(),
sheetPeekHeight: Dp,
sheetMaxWidth: Dp = 640.dp,
sheetShape: Shape = TangemTheme.shapes.bottomSheetLarge,
sheetContainerColor: Color = Color.White,
sheetSwipeEnabled: Boolean = true,
topBar: @Composable (() -> Unit)? = null,
snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
containerColor: Color = TangemTheme.colors.background.secondary,
contentColor: Color = contentColorFor(containerColor),
bottomSheet: @Composable () -> Unit = {},
content: @Composable (PaddingValues) -> Unit,
) {
BottomSheetScaffoldLayout(
@ -118,17 +83,7 @@ fun TangemBottomSheetScaffold(
sheetState = scaffoldState.bottomSheetState,
containerColor = containerColor,
contentColor = contentColor,
bottomSheet = {
StandardBottomSheet(
state = scaffoldState.bottomSheetState,
peekHeight = sheetPeekHeight,
sheetMaxWidth = sheetMaxWidth,
sheetSwipeEnabled = sheetSwipeEnabled,
shape = sheetShape,
containerColor = sheetContainerColor,
content = sheetContent,
)
},
bottomSheet = bottomSheet,
)
}
@ -183,35 +138,31 @@ fun rememberTangemStandardBottomSheetState(
skipHiddenState = skipHiddenState,
)
@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
private fun StandardBottomSheet(
fun CustomBottomSheet(
state: TangemSheetState,
peekHeight: Dp,
sheetMaxWidth: Dp,
sheetSwipeEnabled: Boolean,
shape: Shape,
containerColor: Color,
sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
sheetSwipeEnabled: Boolean = true,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit,
) {
val scope = rememberCoroutineScope()
val orientation = Orientation.Vertical
val peekHeightPx = with(LocalDensity.current) { peekHeight.toPx() }
val nestedScroll =
if (sheetSwipeEnabled) {
Modifier.nestedScroll(
remember(state.anchoredDraggableState) {
consumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
sheetState = state,
orientation = orientation,
onFling = { scope.launch { state.settle(it) } },
)
},
)
} else {
Modifier
}
val nestedScroll = Modifier.conditionalCompose(sheetSwipeEnabled) {
nestedScroll(
remember(state.anchoredDraggableState) {
consumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
sheetState = state,
orientation = orientation,
onFling = { scope.launch { state.settle(it) } },
)
},
)
}
Column(
modifier = Modifier
@ -219,61 +170,17 @@ private fun StandardBottomSheet(
.fillMaxWidth()
.requiredHeightIn(min = peekHeight)
.then(nestedScroll)
.draggableAnchors(
state = state.anchoredDraggableState,
.bottomSheetDraggableAnchor(
state = state,
orientation = orientation,
anchors = { sheetSize, constraints ->
val layoutHeight = constraints.maxHeight.toFloat()
val sheetHeight = sheetSize.height.toFloat()
val newAnchors = DraggableAnchors {
if (!state.skipPartiallyExpanded) {
PartiallyExpanded at (layoutHeight - peekHeightPx)
}
if (sheetHeight != peekHeightPx) {
Expanded at maxOf(layoutHeight - sheetHeight, 0f)
}
if (!state.skipHiddenState) {
Hidden at layoutHeight
}
}
val newTarget =
when (val oldTarget = state.anchoredDraggableState.targetValue) {
Hidden -> if (newAnchors.hasPositionFor(Hidden)) Hidden else oldTarget
PartiallyExpanded ->
when {
newAnchors.hasPositionFor(PartiallyExpanded) -> PartiallyExpanded
newAnchors.hasPositionFor(Expanded) -> Expanded
newAnchors.hasPositionFor(Hidden) -> Hidden
else -> oldTarget
}
Expanded ->
when {
newAnchors.hasPositionFor(Expanded) -> Expanded
newAnchors.hasPositionFor(PartiallyExpanded) -> PartiallyExpanded
newAnchors.hasPositionFor(Hidden) -> Hidden
else -> oldTarget
}
}
newAnchors to newTarget
},
peekHeightPx = peekHeightPx,
)
.anchoredDraggable(
state = state.anchoredDraggableState,
orientation = orientation,
enabled = sheetSwipeEnabled,
)
.softLayerShadow(
radius = 8.dp,
color = Color.Black.copy(
alpha = if (isSystemInDarkTheme()) .16f else .08f
),
shape = shape,
offset = DpOffset(x = 0.dp, y = (-4).dp),
isAlphaContentClip = true
)
.background(containerColor, shape)
.clip(shape),
.then(modifier),
) {
content()
}
@ -292,8 +199,7 @@ private fun BottomSheetScaffoldLayout(
contentColor: Color,
) {
Layout(
contents =
listOf<@Composable () -> Unit>(
contents = listOf(
topBar ?: {},
{
Surface(
@ -331,13 +237,12 @@ private fun BottomSheetScaffoldLayout(
val snackbarWidth = snackbarPlaceables.fastMaxOfOrNull { it.width } ?: 0
val snackbarHeight = snackbarPlaceables.fastMaxOfOrNull { it.height } ?: 0
val snackbarOffsetX = (layoutWidth - snackbarWidth) / 2
val snackbarOffsetY =
when (sheetState.currentValue) {
PartiallyExpanded -> sheetOffset().roundToInt() - snackbarHeight
Expanded,
Hidden,
-> layoutHeight - snackbarHeight
}
val snackbarOffsetY = when (sheetState.currentValue) {
PartiallyExpanded -> sheetOffset().roundToInt() - snackbarHeight
Expanded,
Hidden,
-> layoutHeight - snackbarHeight
}
// Placement order is important for elevation
bodyPlaceables.fastForEach { it.placeRelative(0, topBarHeight) }
@ -346,4 +251,50 @@ private fun BottomSheetScaffoldLayout(
snackbarPlaceables.fastForEach { it.placeRelative(snackbarOffsetX, snackbarOffsetY) }
}
}
}
private fun Modifier.bottomSheetDraggableAnchor(
state: TangemSheetState,
orientation: Orientation,
peekHeightPx: Float,
): Modifier {
return draggableAnchors(
state = state.anchoredDraggableState,
orientation = orientation,
anchors = { sheetSize, constraints ->
val layoutHeight = constraints.maxHeight.toFloat()
val sheetHeight = sheetSize.height.toFloat()
val newAnchors = DraggableAnchors {
if (!state.skipPartiallyExpanded) {
PartiallyExpanded at (layoutHeight - peekHeightPx)
}
if (sheetHeight != peekHeightPx) {
Expanded at maxOf(layoutHeight - sheetHeight, 0f)
}
if (!state.skipHiddenState) {
Hidden at layoutHeight
}
}
val newTarget =
when (val oldTarget = state.anchoredDraggableState.targetValue) {
Hidden -> if (newAnchors.hasPositionFor(Hidden)) Hidden else oldTarget
PartiallyExpanded ->
when {
newAnchors.hasPositionFor(PartiallyExpanded) -> PartiallyExpanded
newAnchors.hasPositionFor(Expanded) -> Expanded
newAnchors.hasPositionFor(Hidden) -> Hidden
else -> oldTarget
}
Expanded ->
when {
newAnchors.hasPositionFor(Expanded) -> Expanded
newAnchors.hasPositionFor(PartiallyExpanded) -> PartiallyExpanded
newAnchors.hasPositionFor(Hidden) -> Hidden
else -> oldTarget
}
}
newAnchors to newTarget
},
)
}

View file

@ -6,7 +6,9 @@ import androidx.compose.ui.graphics.Color
object TangemColorPalette {
// region Base
val Black = Color(0xFF000000)
val BlackZero = Color(0x00000000)
val White = Color(0xFFFFFFFF)
val WhiteZero = Color(0x00FFFFFF)
// endregion Base
// region Dark
@ -17,6 +19,7 @@ object TangemColorPalette {
val Dark5 = Color(0xFF303030)
val Dark6 = Color(0xFF1E1E1E)
val Dark7 = Color(0xFF171717)
val Dark8 = Color(0xFF0F0F0F)
// endregion Dark
// region Dark Alpha

View file

@ -23,6 +23,7 @@ class TangemColors2 internal constructor(
val markers: Markers,
val tabs: Tabs,
val contextMenu: ContextMenu,
val shadow: Shadow,
) {
@Stable
@ -637,6 +638,30 @@ class TangemColors2 internal constructor(
}
}
@Stable
class Shadow internal constructor(
min: Color,
max: Color,
fadeMin: Color,
fadeMax: Color,
) {
var min by mutableStateOf(min)
private set
var max by mutableStateOf(max)
private set
var fadeMin by mutableStateOf(fadeMin)
private set
var fadeMax by mutableStateOf(fadeMax)
private set
fun update(other: Shadow) {
min = other.min
max = other.max
fadeMin = other.fadeMin
fadeMax = other.fadeMax
}
}
fun update(other: TangemColors2) {
text.update(other.text)
graphic.update(other.graphic)
@ -651,5 +676,6 @@ class TangemColors2 internal constructor(
markers.update(other.markers)
tabs.update(other.tabs)
contextMenu.update(other.contextMenu)
shadow.update(other.shadow)
}
}

View file

@ -192,6 +192,12 @@ private fun lightThemeColors2(): TangemColors2 {
val contextMenu = TangemColors2.ContextMenu(
background = TangemColorPalette.Dark_05,
)
val shadow = TangemColors2.Shadow(
min = TangemColorPalette.BlackZero,
max = TangemColorPalette.Dark_20,
fadeMin = TangemColorPalette.WhiteZero,
fadeMax = TangemColorPalette.White,
)
return TangemColors2(
text = text,
graphic = graphic,
@ -206,6 +212,7 @@ private fun lightThemeColors2(): TangemColors2 {
markers = markers,
tabs = tabs,
contextMenu = contextMenu,
shadow = shadow,
)
}
@ -361,6 +368,12 @@ private fun darkThemeColors2(): TangemColors2 {
val contextMenu = TangemColors2.ContextMenu(
background = TangemColorPalette.Light_10,
)
val shadow = TangemColors2.Shadow(
min = TangemColorPalette.BlackZero,
max = TangemColorPalette.Dark8,
fadeMin = TangemColorPalette.BlackZero,
fadeMax = TangemColorPalette.Black,
)
return TangemColors2(
text = text,
graphic = graphic,
@ -375,5 +388,6 @@ private fun darkThemeColors2(): TangemColors2 {
markers = markers,
tabs = tabs,
contextMenu = contextMenu,
shadow = shadow,
)
}

View file

@ -193,6 +193,7 @@ private fun BadgeTypeRow(
color = color,
type = type,
iconPosition = TangemBadgeIconPosition.Start,
onClick = {},
)
}
Box(
@ -205,6 +206,7 @@ private fun BadgeTypeRow(
shape = shape,
color = color,
type = type,
onClick = {},
)
}
Box(
@ -217,7 +219,7 @@ private fun BadgeTypeRow(
shape = shape,
color = color,
type = type,
iconPosition = TangemBadgeIconPosition.Start,
onClick = {},
)
}
}

View file

@ -15,8 +15,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
@ -28,10 +26,10 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.ds.button.TangemButton
@ -75,7 +73,6 @@ internal fun OrganizeTokensContent(
onDismissRequest = onDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
addBottomInsets = false,
containerColor = TangemTheme.colors2.surface.level2,
title = {
TangemTopBar(
@ -99,11 +96,14 @@ internal fun OrganizeTokensContent(
},
)
},
footer = {
BottomButtons(organizeTokensUM = organizeTokensUM)
},
content = {
TokenList(
organizeTokensUM = organizeTokensUM,
dragAndDropIntents = dragAndDropIntents,
modifier = Modifier.hazeSourceTangem(hazeState),
modifier = Modifier.hazeSourceTangem(hazeState, zIndex = -1f),
)
},
)
@ -121,9 +121,7 @@ private fun TokenList(
val hapticFeedback = LocalHapticFeedback.current
val tokenList = organizeTokensUM.tokenList
Box(
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors2.surface.level2),
modifier = modifier.background(TangemTheme.colors2.surface.level2),
) {
val onDragEnd: (Int, Int) -> Unit = remember {
{ _, _ ->
@ -175,20 +173,11 @@ private fun TokenList(
isBalanceHidden = organizeTokensUM.isBalanceHidden,
)
}
item {
SpacerH(TangemTheme.dimens2.x20)
}
}
BottomFade(
gradientBrush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
TangemTheme.colors2.surface.level2.copy(0.9f),
TangemTheme.colors2.surface.level2,
),
),
modifier = Modifier.align(Alignment.BottomCenter),
)
BottomButtons(organizeTokensUM = organizeTokensUM)
}
}

View file

@ -11,6 +11,7 @@ import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
@ -20,16 +21,17 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.*
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
@ -40,19 +42,16 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.*
import androidx.paging.compose.collectAsLazyPagingItems
import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheet
import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheet
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.expressTransactionsItems
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.components.atoms.handComposableComponentHeight
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetDraggableHeaderLegacy
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer
import com.tangem.core.ui.components.rememberIsKeyboardVisible
@ -61,6 +60,7 @@ import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar
import com.tangem.core.ui.components.snackbar.TangemSnackbar
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.softLayerShadow
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalWindowSize
@ -290,15 +290,8 @@ private inline fun BaseScaffoldWithMarkets(
crossinline bottomSheetContent: @Composable () -> Unit,
crossinline content: @Composable (PaddingValues) -> Unit,
) {
val bottomSheetState = rememberTangemStandardBottomSheetState()
val isKeyboardVisible by rememberIsKeyboardVisible()
val scaffoldState = rememberTangemBottomSheetScaffoldState(
bottomSheetState = bottomSheetState,
snackbarHostState = snackbarHostState,
)
val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(density = this).toDp() }
val statusBarHeight = with(density) { WindowInsets.statusBars.getTop(density = this).toDp() }
@ -308,6 +301,12 @@ private inline fun BaseScaffoldWithMarkets(
val coroutineScope = rememberCoroutineScope()
val background = TangemTheme.colors.background.tertiary
val bottomSheetState = rememberTangemStandardBottomSheetState()
val scaffoldState = rememberTangemBottomSheetScaffoldState(
bottomSheetState = bottomSheetState,
snackbarHostState = snackbarHostState,
)
val showMarketsHint by remember {
derivedStateOf {
// Show hint only when there are items in the list
@ -321,7 +320,7 @@ private inline fun BaseScaffoldWithMarkets(
CompositionLocalProvider(
LocalMainBottomSheetColor provides remember(background) { mutableStateOf(background) },
) {
val backgroundColor = LocalMainBottomSheetColor.current
val backgroundColor by LocalMainBottomSheetColor.current
var isSearchFieldFocused by remember { mutableStateOf(false) }
val isNavBarVisible = remember { mutableStateOf(true) }
@ -343,43 +342,61 @@ private inline fun BaseScaffoldWithMarkets(
.navigationBarsPadding(),
)
},
containerColor = TangemTheme.colors.background.secondary,
sheetContainerColor = backgroundColor.value,
scaffoldState = scaffoldState,
sheetPeekHeight = peekHeight,
sheetShape = TangemTheme.shapes.bottomSheetLarge,
sheetContent = {
// hide bottom sheet when back pressed
BackHandler(
isKeyboardVisible.not() &&
bottomSheetState.currentValue == TangemSheetValue.Expanded,
) {
coroutineScope.launch { bottomSheetState.partialExpand() }
}
Column(
containerColor = TangemTheme.colors.background.secondary,
scaffoldState = scaffoldState,
bottomSheet = {
CustomBottomSheet(
state = scaffoldState.bottomSheetState,
peekHeight = peekHeight,
modifier = Modifier
// expand bottom sheet when clicked on the header
.clickable(
enabled = bottomSheetState.currentValue == TangemSheetValue.PartiallyExpanded,
indication = null,
interactionSource = null,
.softLayerShadow(
radius = 8.dp,
color = Color.Black.copy(
alpha = if (isSystemInDarkTheme()) .16f else .08f,
),
shape = TangemTheme.shapes.bottomSheetLarge,
offset = DpOffset(x = 0.dp, y = (-4).dp),
isAlphaContentClip = true,
)
.clip(TangemTheme.shapes.bottomSheetLarge)
.background(backgroundColor),
content = {
// hide bottom sheet when back pressed
BackHandler(
isKeyboardVisible.not() &&
bottomSheetState.currentValue == TangemSheetValue.Expanded,
) {
coroutineScope.launch { bottomSheetState.expand() }
coroutineScope.launch { bottomSheetState.partialExpand() }
}
.sizeIn(maxHeight = maxHeight - statusBarHeight),
) {
Hand(Modifier.drawBehind { drawRect(backgroundColor.value) })
Box(
modifier = Modifier
.onFocusChanged {
isSearchFieldFocused = it.isFocused
},
) {
bottomSheetContent()
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
// expand bottom sheet when clicked on the header
.clickable(
enabled = bottomSheetState.currentValue == TangemSheetValue.PartiallyExpanded,
indication = null,
interactionSource = null,
) {
coroutineScope.launch { bottomSheetState.expand() }
}
.sizeIn(maxHeight = maxHeight - statusBarHeight),
) {
TangemBottomSheetDraggableHeaderLegacy(backgroundColor)
Box(
modifier = Modifier
.onFocusChanged {
isSearchFieldFocused = it.isFocused
},
) {
bottomSheetContent()
}
}
},
)
},
content = { paddingValues ->
Box {
@ -428,7 +445,7 @@ private inline fun BaseScaffoldWithMarkets(
Box(
Modifier
.align(Alignment.BottomCenter)
.background(backgroundColor.value)
.background(backgroundColor)
.height(bottomBarHeight)
.fillMaxWidth(),
)

View file

@ -17,6 +17,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
@ -25,8 +26,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
@ -39,9 +42,10 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.atoms.handComposableComponentHeight
import com.tangem.core.ui.components.background.northernlights.NorthernLightsBackground
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetDraggableHeader
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshSlidingContainer
import com.tangem.core.ui.components.haze.hazeSourceTangem
@ -295,25 +299,20 @@ private inline fun BaseScaffoldWithMarkets(
crossinline bottomSheetContent: @Composable () -> Unit,
crossinline content: @Composable (PaddingValues, TangemSheetState) -> Unit,
) {
val bottomSheetState = rememberTangemStandardBottomSheetState()
val isKeyboardVisible by rememberIsKeyboardVisible()
val scaffoldState = rememberTangemBottomSheetScaffoldState(bottomSheetState = bottomSheetState)
val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(density = this).toDp() }
val statusBarHeight = with(density) { WindowInsets.statusBars.getTop(density = this).toDp() }
val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight
val maxHeight = LocalWindowSize.current.height
val peekHeight = bottomSheetHeaderHeightProvider() + TangemTheme.dimens2.x3 + bottomBarHeight
val coroutineScope = rememberCoroutineScope()
val background = TangemTheme.colors2.surface.level3
val bottomSheetState = rememberTangemStandardBottomSheetState()
val scaffoldState = rememberTangemBottomSheetScaffoldState(bottomSheetState = bottomSheetState)
CompositionLocalProvider(
LocalMainBottomSheetColor provides remember(background) { mutableStateOf(background) },
) {
val backgroundColor = LocalMainBottomSheetColor.current
val backgroundColor by LocalMainBottomSheetColor.current
var isSearchFieldFocused by remember { mutableStateOf(false) }
val isNavBarVisible = remember { mutableStateOf(true) }
@ -327,41 +326,18 @@ private inline fun BaseScaffoldWithMarkets(
Box(modifier = modifier) {
TangemBottomSheetScaffold(
containerColor = Color.Unspecified,
sheetContainerColor = backgroundColor.value,
scaffoldState = scaffoldState,
sheetPeekHeight = peekHeight,
sheetShape = TangemTheme.shapes.bottomSheetLarge,
sheetContent = {
// hide bottom sheet when back pressed
BackHandler(
isKeyboardVisible.not() &&
bottomSheetState.currentValue == TangemSheetValue.Expanded,
bottomSheet = {
BottomSheet(
bottomSheetState = bottomSheetState,
backgroundColor = backgroundColor,
peekHeight = peekHeight,
onFocusChange = { focusState ->
isSearchFieldFocused = focusState.isFocused
},
) {
coroutineScope.launch { bottomSheetState.partialExpand() }
}
Column(
modifier = Modifier
// expand bottom sheet when clicked on the header
.clickable(
enabled = bottomSheetState.currentValue == TangemSheetValue.PartiallyExpanded,
indication = null,
interactionSource = null,
) {
coroutineScope.launch { bottomSheetState.expand() }
}
.sizeIn(maxHeight = maxHeight - statusBarHeight),
) {
Hand(Modifier.drawBehind { drawRect(backgroundColor.value) })
Box(
modifier = Modifier
.onFocusChanged {
isSearchFieldFocused = it.isFocused
},
) {
bottomSheetContent()
}
bottomSheetContent()
}
},
content = { paddingValues ->
@ -385,7 +361,7 @@ private inline fun BaseScaffoldWithMarkets(
Box(
Modifier
.align(Alignment.BottomCenter)
.background(backgroundColor.value)
.background(backgroundColor)
.height(bottomBarHeight)
.fillMaxWidth(),
)
@ -400,6 +376,81 @@ private inline fun BaseScaffoldWithMarkets(
}
}
@Composable
private fun BottomSheet(
bottomSheetState: TangemSheetState,
backgroundColor: Color,
peekHeight: Dp,
onFocusChange: (FocusState) -> Unit,
bottomSheetContent: @Composable () -> Unit,
) {
val isKeyboardVisible by rememberIsKeyboardVisible()
val coroutineScope = rememberCoroutineScope()
val density = LocalDensity.current
val statusBarHeight = with(density) { WindowInsets.statusBars.getTop(density = this).toDp() }
val maxHeight = LocalWindowSize.current.height
val shape = RoundedCornerShape(
topStart = TangemTheme.dimens2.x8,
topEnd = TangemTheme.dimens2.x8,
)
CustomBottomSheet(
state = bottomSheetState,
peekHeight = peekHeight,
content = {
// hide bottom sheet when back pressed
BackHandler(
isKeyboardVisible.not() &&
bottomSheetState.currentValue == TangemSheetValue.Expanded,
) {
coroutineScope.launch { bottomSheetState.partialExpand() }
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
// expand bottom sheet when clicked on the header
.clickable(
enabled = bottomSheetState.currentValue == TangemSheetValue.PartiallyExpanded,
indication = null,
interactionSource = null,
) {
coroutineScope.launch { bottomSheetState.expand() }
}
.sizeIn(maxHeight = maxHeight - statusBarHeight),
) {
Box(modifier = Modifier.fillMaxWidth()) {
BottomFade(
gradientBrush = Brush.verticalGradient(
colors = listOf(
TangemTheme.colors2.shadow.min,
TangemTheme.colors2.shadow.max,
),
),
modifier = Modifier
.offset(y = TangemTheme.dimens2.x5.unaryMinus()),
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
TangemBottomSheetDraggableHeader()
Box(
modifier = Modifier
.fillMaxWidth()
.clip(shape)
.background(backgroundColor)
.onFocusChanged(onFocusChange),
) {
bottomSheetContent()
}
}
}
}
},
)
}
@Composable
private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: () -> Unit) {
val alpha by animateFloatAsState(