Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-01 16:04:47 +05:00
parent 304dfb3205
commit 93f6d5f2cc
2 changed files with 393 additions and 0 deletions

View file

@ -0,0 +1,262 @@
package com.tangem.core.ui.components.bottomsheets.modal
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.material3.SheetValue.Expanded
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.internal.ModalBottomSheetWithBackHandling
import com.tangem.core.ui.components.bottomsheets.internal.collapse
import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.WindowInsetsZero
const val MODAL_SHEET_MAX_HEIGHT = 0.8f
/**
* Modal bottom sheet with [content] and optional [title].
*
* Maximum height of sheet is 80% screen height
*
* [Show in Figma](https://www.figma.com/design/09KKG4ZVuFDZhj8WLv5rGJ/%F0%9F%9A%A7-App-experience?node-id=3254-61208&t=vixa7id6ggALcxfF-4)
*/
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> TangemModalBottomSheet(
config: TangemBottomSheetConfig,
containerColor: Color = TangemTheme.colors.background.primary,
skipPartiallyExpanded: Boolean = true,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable BoxScope.(T) -> Unit = {},
crossinline content: @Composable ColumnScope.(T) -> Unit,
) {
val isAlwaysVisible = LocalBottomSheetAlwaysVisible.current
if (isAlwaysVisible) {
PreviewModalBottomSheet<T>(
config = config,
containerColor = containerColor,
title = title,
content = content,
skipPartiallyExpanded = skipPartiallyExpanded,
)
} else {
DefaultModalBottomSheet<T>(
config = config,
containerColor = containerColor,
title = title,
content = content,
onBack = onBack,
skipPartiallyExpanded = skipPartiallyExpanded,
)
}
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
inline fun <reified T : TangemBottomSheetConfigContent> DefaultModalBottomSheet(
config: TangemBottomSheetConfig,
containerColor: Color,
skipPartiallyExpanded: Boolean = true,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
) {
var isVisible by remember { mutableStateOf(value = config.isShown) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
if (isVisible && config.content is T) {
BasicModalBottomSheet<T>(
config = config,
sheetState = sheetState,
containerColor = containerColor,
title = title,
onBack = onBack,
content = content,
)
}
LaunchedEffect(key1 = config.isShown) {
if (config.isShown) {
isVisible = true
} else {
sheetState.collapse { isVisible = false }
}
}
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheet(
config: TangemBottomSheetConfig,
containerColor: Color,
skipPartiallyExpanded: Boolean = true,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
) {
BasicModalBottomSheet<T>(
config = config,
sheetState = SheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
initialValue = Expanded,
density = LocalDensity.current,
),
onBack = null,
containerColor = containerColor,
title = title,
content = content,
)
}
@Suppress("LongParameterList")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheet(
config: TangemBottomSheetConfig,
sheetState: SheetState,
containerColor: Color,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
modifier: Modifier = Modifier,
) {
val model = config.content as? T ?: return
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val bsContent: @Composable ColumnScope.() -> Unit = {
val maxHeight = LocalConfiguration.current.screenHeightDp * MODAL_SHEET_MAX_HEIGHT
Column(
modifier = Modifier
.systemBarsPadding()
.padding(horizontal = 8.dp, vertical = 8.dp)
.clip(TangemTheme.shapes.roundedCornersLarge)
.background(containerColor)
.heightIn(max = maxHeight.dp)
.padding(bottom = bottomBarHeight)
.fillMaxWidth(),
) {
Box(modifier = Modifier.fillMaxWidth()) {
title(model)
}
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
content(model)
}
}
}
if (onBack != null) {
ModalBottomSheetWithBackHandling(
modifier = modifier,
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = Color.Transparent,
shape = TangemTheme.shapes.roundedCornersLarge,
contentWindowInsets = { WindowInsetsZero },
onBack = onBack,
dragHandle = null,
content = bsContent,
)
} else {
ModalBottomSheet(
modifier = modifier,
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = Color.Transparent,
shape = TangemTheme.shapes.roundedCornersLarge,
contentWindowInsets = { WindowInsetsZero },
dragHandle = null,
content = bsContent,
)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360, heightDp = 800)
@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemModalBottomSheet_Preview() {
TangemThemePreview {
TangemModalBottomSheet<TangemBottomSheetConfigContentPreviewConfig>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = TangemBottomSheetConfigContentPreviewConfig(),
),
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)
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = "Go it",
onClick = {},
)
}
},
)
}
}
private class TangemBottomSheetConfigContentPreviewConfig : TangemBottomSheetConfigContent
// endregion

View file

@ -0,0 +1,131 @@
package com.tangem.core.ui.components.bottomsheets.modal
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.R
import com.tangem.core.ui.components.buttons.small.TangemIconButton
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* Title component for [TangemModalBottomSheet] with [TangemIconButton] for buttons.
*/
@Composable
fun TangemModalBottomSheetTitle(
modifier: Modifier = Modifier,
title: TextReference? = null,
@DrawableRes startIconRes: Int? = null,
onStartClick: (() -> Unit)? = null,
@DrawableRes endIconRes: Int? = null,
onEndClick: (() -> Unit)? = null,
) {
Box(
modifier = modifier
.fillMaxWidth()
.height(56.dp),
) {
if (startIconRes != null && onStartClick != null) {
TangemIconButton(
iconRes = startIconRes,
onClick = onStartClick,
modifier = Modifier
.padding(16.dp)
.align(Alignment.CenterStart),
)
}
if (title != null) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.align(Alignment.Center),
)
}
if (endIconRes != null && onEndClick != null) {
TangemIconButton(
iconRes = endIconRes,
onClick = onEndClick,
modifier = Modifier
.padding(16.dp)
.align(Alignment.CenterEnd),
)
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_TangemModalBottomSheetTitle(
@PreviewParameter(TangemModalBottomSheetTitleProvider::class) params: TangemModalBottomSheetTitleData,
) {
TangemThemePreview {
TangemModalBottomSheetTitle(
title = params.title,
startIconRes = params.startIconRes,
onStartClick = params.onStartClick,
endIconRes = params.endIconRes,
onEndClick = params.onEndClick,
modifier = Modifier.background(TangemTheme.colors.background.secondary),
)
}
}
private data class TangemModalBottomSheetTitleData(
val title: TextReference?,
val startIconRes: Int?,
val onStartClick: (() -> Unit)?,
val endIconRes: Int?,
val onEndClick: (() -> Unit)?,
)
private class TangemModalBottomSheetTitleProvider : PreviewParameterProvider<TangemModalBottomSheetTitleData> {
override val values = sequenceOf(
TangemModalBottomSheetTitleData(
title = resourceReference(R.string.wallet_title),
startIconRes = R.drawable.ic_back_24,
onStartClick = {},
endIconRes = null,
onEndClick = null,
),
TangemModalBottomSheetTitleData(
title = resourceReference(R.string.wallet_title),
startIconRes = null,
onStartClick = null,
endIconRes = R.drawable.ic_close_24,
onEndClick = {},
),
TangemModalBottomSheetTitleData(
title = resourceReference(R.string.wallet_title),
startIconRes = R.drawable.ic_back_24,
onStartClick = {},
endIconRes = R.drawable.ic_close_24,
onEndClick = {},
),
TangemModalBottomSheetTitleData(
title = null,
startIconRes = R.drawable.ic_back_24,
onStartClick = {},
endIconRes = R.drawable.ic_close_24,
onEndClick = {},
),
)
}
// endregion Preview