From 8411911466def0eac29b33d5385f35d30dff53cf Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Mar 2026 15:16:56 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../bottomsheets/TangemBottomSheet.kt | 440 ++++++++++++++++++ .../internal/InternalBottomSheet.kt | 88 ++++ .../ModalBottomSheetWithBackHandling.kt | 47 +- .../bottomsheets/sheet/TangemBottomSheet.kt | 2 +- ...TangemBottomSheetDraggableHeaderLegacy.kt} | 20 +- .../TangemBottomSheetScaffold.kt | 227 ++++----- .../tangem/core/ui/res/TangemColorPalette.kt | 3 + .../com/tangem/core/ui/res/TangemColors2.kt | 26 ++ .../tangem/core/ui/res/TangemThemeRedesign.kt | 14 + .../storybook/page/badge/TangemBadgeStory.kt | 4 +- .../ui/OrganizeTokensContent.kt | 33 +- .../presentation/wallet/ui/WalletScreen.kt | 117 +++-- .../presentation/wallet/ui/WalletScreen2.kt | 141 ++++-- 13 files changed, 858 insertions(+), 304 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/internal/InternalBottomSheet.kt rename core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/{TangemBottomSheetDraggableHeader.kt => TangemBottomSheetDraggableHeaderLegacy.kt} (61%) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt new file mode 100644 index 0000000000..2eceba7401 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt @@ -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 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( + config = config, + containerColor = containerColor, + type = type, + title = title, + content = content, + footer = footer, + skipPartiallyExpanded = skipPartiallyExpanded, + ) + } else { + DefaultModalBottomSheetWithFooter( + config = config, + containerColor = containerColor, + type = type, + title = title, + content = content, + footer = footer, + onBack = onBack, + skipPartiallyExpanded = skipPartiallyExpanded, + ) + } +} + +@Suppress("LongParameterList") +@Composable +@OptIn(ExperimentalMaterial3Api::class) +inline fun 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( + 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 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( + 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 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( + 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( + 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 { + override val values: Sequence + get() = sequenceOf( + Default, + Modal, + ) +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/internal/InternalBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/internal/InternalBottomSheet.kt new file mode 100644 index 0000000000..6d311677d4 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/internal/InternalBottomSheet.kt @@ -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() } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/internal/ModalBottomSheetWithBackHandling.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/internal/ModalBottomSheetWithBackHandling.kt index a62e171f5f..b4a3b4c092 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/internal/ModalBottomSheetWithBackHandling.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/internal/ModalBottomSheetWithBackHandling.kt @@ -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() } - } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt index d609afe29a..bf321e0650 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt @@ -188,7 +188,7 @@ inline fun 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, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetDraggableHeader.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetDraggableHeaderLegacy.kt similarity index 61% rename from core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetDraggableHeader.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetDraggableHeaderLegacy.kt index 66b0ae6033..6a03c12c26 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetDraggableHeader.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetDraggableHeaderLegacy.kt @@ -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), + ), + ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemBottomSheetScaffold.kt b/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemBottomSheetScaffold.kt index 7c07f422bc..4826155d48 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemBottomSheetScaffold.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/sheetscaffold/TangemBottomSheetScaffold.kt @@ -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 + }, + ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt index 9b81ff94d3..5b4ef02f7d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt @@ -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 diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt index 61f297696c..0cd61de19c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt @@ -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) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt index ad14b94dbf..619a08d5b8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt @@ -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, ) } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/TangemBadgeStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/TangemBadgeStory.kt index 96f315a25c..c3c6cea2a1 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/TangemBadgeStory.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/TangemBadgeStory.kt @@ -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 = {}, ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensContent.kt index 1de1a10cab..120fd7a0b8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensContent.kt @@ -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) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 5eb943f097..4f12a59f17 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -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(), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt index 83c59d731f..0d552f2757 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt @@ -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(