Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-10 17:19:43 +04:00
parent b3fe4cbe05
commit e1ee680173
4 changed files with 230 additions and 39 deletions

View file

@ -1,11 +1,18 @@
package com.tangem.core.ui.components.bottomsheets
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue.Expanded
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.extensions.TextReference
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.utils.WindowInsetsZero
@ -13,45 +20,79 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
/**
* Tangem bottom sheet with custom draggable header and config
*
* @param config data model containing logic and ui models
* @param
* @param content custom bottom sheet content
* Bottom sheet with [content], [titleText] and optional [titleAction].
* */
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
config: TangemBottomSheetConfig,
titleText: TextReference,
titleAction: TopAppBarButtonUM? = null,
containerColor: Color = TangemTheme.colors.background.primary,
addBottomInsets: Boolean = true,
crossinline content: @Composable ColumnScope.(T) -> Unit,
) {
TangemBottomSheet(
config = config,
containerColor = containerColor,
addBottomInsets = addBottomInsets,
title = { TangemBottomSheetTitle(title = titleText, endButton = titleAction) },
content = content,
)
}
/**
* Bottom sheet with [content] and optional [title].
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
config: TangemBottomSheetConfig,
containerColor: Color = TangemTheme.colors.background.primary,
addBottomInsets: Boolean = true,
crossinline title: @Composable BoxScope.(T) -> Unit = {},
crossinline content: @Composable ColumnScope.(T) -> Unit,
) {
val isAlwaysVisible = LocalBottomSheetAlwaysVisible.current
if (isAlwaysVisible) {
PreviewBottomSheet<T>(
config = config,
containerColor = containerColor,
addBottomInsets = addBottomInsets,
title = title,
content = content,
)
} else {
DefaultBottomSheet<T>(
config = config,
containerColor = containerColor,
addBottomInsets = addBottomInsets,
title = title,
content = content,
)
}
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
inline fun <reified T : TangemBottomSheetConfigContent> DefaultBottomSheet(
config: TangemBottomSheetConfig,
containerColor: Color,
addBottomInsets: Boolean,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
) {
var isVisible by remember { mutableStateOf(value = config.isShow) }
val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
if (isVisible && config.content is T) {
ModalBottomSheet(
// FIXME temporary solution to fix height of the bottom sheet
modifier = Modifier.sizeIn(maxHeight = LocalWindowSize.current.height - statusBarHeight),
onDismissRequest = config.onDismissRequest,
BasicBottomSheet<T>(
config = config,
sheetState = sheetState,
containerColor = containerColor,
shape = TangemTheme.shapes.bottomSheetLarge,
windowInsets = WindowInsetsZero,
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
) {
if (addBottomInsets) {
Column(
// FIXME temporary solution to fix height of the bottom sheet
modifier = Modifier.navigationBarsPadding(),
) {
content(config.content)
}
} else {
content(config.content)
}
}
addBottomInsets = addBottomInsets,
title = title,
content = content,
)
}
LaunchedEffect(key1 = config.isShow) {
@ -63,6 +104,74 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
}
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
inline fun <reified T : TangemBottomSheetConfigContent> PreviewBottomSheet(
config: TangemBottomSheetConfig,
containerColor: Color,
addBottomInsets: Boolean,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
) {
BasicBottomSheet<T>(
config = config,
sheetState = SheetState(
skipPartiallyExpanded = true,
initialValue = Expanded,
density = LocalDensity.current,
),
containerColor = containerColor,
addBottomInsets = addBottomInsets,
title = title,
content = content,
)
}
@Suppress("LongParameterList")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
config: TangemBottomSheetConfig,
sheetState: SheetState,
containerColor: Color,
addBottomInsets: Boolean,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
) {
val model = config.content as? T ?: return
val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
ModalBottomSheet(
// FIXME temporary solution to fix height of the bottom sheet
modifier = Modifier.sizeIn(maxHeight = LocalWindowSize.current.height - statusBarHeight),
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = containerColor,
shape = TangemTheme.shapes.bottomSheetLarge,
windowInsets = WindowInsetsZero,
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
) {
Column(
modifier = Modifier.let {
if (addBottomInsets) {
it.navigationBarsPadding()
} else {
it
}
},
) {
Box(
modifier = Modifier.fillMaxWidth(),
) {
title(model)
}
content(model)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
suspend fun SheetState.collapse(onCollapsed: () -> Unit) {
coroutineScope {

View file

@ -5,7 +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.material.Surface
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -14,8 +14,7 @@ import com.tangem.core.ui.res.TangemTheme
@Composable
fun TangemBottomSheetDraggableHeader(color: Color = TangemTheme.colors.background.primary) {
Surface(
modifier = Modifier
.height(TangemTheme.dimens.size20),
modifier = Modifier.height(TangemTheme.dimens.size20),
color = color,
) {
Box(

View file

@ -0,0 +1,68 @@
package com.tangem.core.ui.components.bottomsheets
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Composable
fun TangemBottomSheetTitle(
title: TextReference?,
modifier: Modifier = Modifier,
endButton: TopAppBarButtonUM? = null,
containerColor: Color = Color.Transparent,
) {
TangemBottomSheetTitle(
modifier = modifier,
title = title?.resolveReference(),
endButton = endButton,
containerColor = containerColor,
)
}
@Composable
fun TangemBottomSheetTitle(
title: String?,
modifier: Modifier = Modifier,
endButton: TopAppBarButtonUM? = null,
containerColor: Color = Color.Transparent,
) {
TangemTopAppBar(
modifier = modifier,
title = title,
endButton = endButton,
textColor = TangemTheme.colors.text.primary1,
iconTint = TangemTheme.colors.icon.informative,
titleAlignment = Alignment.CenterHorizontally,
containerColor = containerColor,
height = TangemTopAppBarHeight.BOTTOM_SHEET,
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_TangemBottomSheetTitle() {
TangemThemePreview {
TangemBottomSheetTitle(
title = "Title",
endButton = TopAppBarButtonUM(
iconRes = R.drawable.ic_information_24,
onIconClicked = {},
),
containerColor = TangemTheme.colors.background.secondary,
)
}
}
// endregion Preview

View file

@ -3,6 +3,9 @@ package com.tangem.core.ui.res
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.platform.LocalHapticFeedback
import com.tangem.core.ui.haptic.MockHapticManager
import com.tangem.core.ui.windowsize.rememberWindowSizePreview
@ -12,18 +15,30 @@ fun TangemThemePreview(
isDark: Boolean? = null,
typography: TangemTypography = TangemTheme.typography,
dimens: TangemDimens = TangemTheme.dimens,
alwaysShowBottomSheets: Boolean = true,
content: @Composable () -> Unit,
) {
val isDarkTheme = isDark ?: isSystemInDarkTheme()
BoxWithConstraints {
TangemTheme(
isDark = isDarkTheme,
typography = typography,
dimens = dimens,
windowSize = rememberWindowSizePreview(maxWidth, maxHeight),
hapticManager = MockHapticManager(LocalHapticFeedback.current),
content = content,
)
CompositionLocalProvider(
LocalBottomSheetAlwaysVisible provides alwaysShowBottomSheets,
) {
BoxWithConstraints {
TangemTheme(
isDark = isDarkTheme,
typography = typography,
dimens = dimens,
windowSize = rememberWindowSizePreview(maxWidth, maxHeight),
hapticManager = MockHapticManager(LocalHapticFeedback.current),
content = content,
)
}
}
}
/**
* This is used to make the bottom sheet always visible in the Preview and should be `true` only in the Preview.
* */
val LocalBottomSheetAlwaysVisible: ProvidableCompositionLocal<Boolean> = compositionLocalOf {
false
}