Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-29 16:01:28 +02:00
parent 8511623421
commit 7095ceec0f
21 changed files with 717 additions and 183 deletions

View file

@ -10,10 +10,11 @@ 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.layout.onGloballyPositioned
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
@ -36,6 +37,14 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.utils.WindowInsetsZero
/**
* Extra bottom inset that scrollable content under a [BasicBottomSheet] should reserve so it
* does not collide with the footer overlay: measured footer height plus the bottom gradient
* (see `gradientHeight` in [FooterOverlay]). Equals `0.dp` when there is no footer or when read
* outside [BasicBottomSheet].
*/
val LocalTangemBottomSheetContentBottomInset = compositionLocalOf { 0.dp }
/**
* Type of [TangemBottomSheet] that defines its behavior and appearance.
* - [Default]: Standard bottom sheet with a draggable header
@ -204,6 +213,9 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
) {
val model = config.content as? T ?: return
val windowSize = LocalWindowSize.current
val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }
var footerHeightDp by remember { mutableStateOf<Dp?>(null) }
val bsContent: @Composable ColumnScope.() -> Unit = {
val maxHeight = when (type) {
@ -212,17 +224,19 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
}
val contentModifier = when (type) {
Default -> Modifier.clip(
RoundedCornerShape(
topStart = TangemTheme.dimens2.x8,
topEnd = TangemTheme.dimens2.x8,
),
)
Default -> Modifier
.padding(bottom = bottomBarHeight)
.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,
bottom = bottomBarHeight,
)
.clip(RoundedCornerShape(TangemTheme.dimens2.x8))
}
@ -236,26 +250,19 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
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,
),
),
FooterOverlay(
measuredFooterHeight = footerHeightDp,
onMeasureFooter = { newHeight ->
if (newHeight != footerHeightDp) {
footerHeightDp = newHeight
}
},
footer = { footer(model) },
content = { content(model) },
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
) {
if (footer != null) {
footer(model)
}
} else {
content(model)
}
}
}
@ -274,6 +281,57 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
)
}
@Composable
fun BoxScope.FooterOverlay(
measuredFooterHeight: Dp?,
onMeasureFooter: (Dp) -> Unit,
footer: @Composable BoxScope.() -> Unit,
content: @Composable () -> Unit,
) {
val density = LocalDensity.current
val gradientHeight = TangemTheme.dimens2.x10
val isFooterRendered = measuredFooterHeight == null || measuredFooterHeight > 0.dp
val contentBottomOverlayHeight = if (isFooterRendered) {
(measuredFooterHeight ?: 0.dp) + gradientHeight
} else {
0.dp
}
val fadeMax = TangemTheme.colors2.surface.level2
CompositionLocalProvider(
LocalTangemBottomSheetContentBottomInset provides contentBottomOverlayHeight,
) {
content()
}
if (isFooterRendered) {
Column(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
) {
Fade(
backgroundColor = fadeMax,
height = gradientHeight,
)
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(measuredFooterHeight ?: 0.dp)
.background(fadeMax),
)
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.onGloballyPositioned { coordinates ->
onMeasureFooter(with(density) { coordinates.size.height.toDp() })
},
) {
footer()
}
}
// region Preview
@Suppress("LongMethod")
@Composable

View file

@ -46,6 +46,7 @@ fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) {
color = badgeUM.color,
type = badgeUM.type,
iconPosition = badgeUM.iconPosition,
shouldRespectIconTint = badgeUM.shouldRespectIconTint,
onClick = badgeUM.onClick,
modifier = modifier,
)
@ -77,6 +78,7 @@ fun TangemBadge(
color: TangemBadgeColor = TangemBadgeColor.Gray,
type: TangemBadgeType = TangemBadgeType.Solid,
iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.None,
shouldRespectIconTint: Boolean = false,
onClick: (() -> Unit)? = null,
) {
val iconColor = getIconColor(type = type, color = color)
@ -95,6 +97,7 @@ fun TangemBadge(
iconPosition = iconPosition,
size = size,
iconColor = iconColor,
shouldRespectIconTint = shouldRespectIconTint,
)
AnimatedVisibility(
visible = text != null,
@ -113,6 +116,7 @@ fun TangemBadge(
iconPosition = iconPosition,
size = size,
iconColor = iconColor,
shouldRespectIconTint = shouldRespectIconTint,
)
}
}
@ -123,6 +127,7 @@ private fun StartIcon(
size: TangemBadgeSize,
iconColor: Color,
tangemIconUM: TangemIconUM? = null,
shouldRespectIconTint: Boolean = false,
) {
AnimatedVisibility(
visible = tangemIconUM != null && iconPosition != TangemBadgeIconPosition.End,
@ -139,7 +144,11 @@ private fun StartIcon(
is TangemIconUM.Url,
TangemIconUM.Empty,
-> wrappedIconRes
is TangemIconUM.Icon -> wrappedIconRes.copy(tintReference = ColorReference2 { iconColor })
is TangemIconUM.Icon -> if (shouldRespectIconTint) {
wrappedIconRes
} else {
wrappedIconRes.copy(tintReference = ColorReference2 { iconColor })
}
},
)
}
@ -151,6 +160,7 @@ private fun EndIcon(
size: TangemBadgeSize,
iconColor: Color,
tangemIconUM: TangemIconUM? = null,
shouldRespectIconTint: Boolean = false,
) {
AnimatedVisibility(
visible = tangemIconUM != null && iconPosition == TangemBadgeIconPosition.End,
@ -167,7 +177,11 @@ private fun EndIcon(
is TangemIconUM.Url,
TangemIconUM.Empty,
-> wrappedIconRes
is TangemIconUM.Icon -> wrappedIconRes.copy(tintReference = ColorReference2 { iconColor })
is TangemIconUM.Icon -> if (shouldRespectIconTint) {
wrappedIconRes
} else {
wrappedIconRes.copy(tintReference = ColorReference2 { iconColor })
}
},
)
}

View file

@ -7,14 +7,16 @@ import com.tangem.core.ui.extensions.TextReference
/**
* UI model for [TangemBadge] component
*
* @param text TextReference for the badge label.
* @param tangemIconUM Model of representation for the icon to be displayed in the badge.
* @param size [TangemBadgeSize] defining the size of the badge.
* @param shape [TangemBadgeShape] defining the shape of the badge.
* @param color [TangemBadgeColor] defining the color scheme of the badge.
* @param type [TangemBadgeType] defining the style of the badge.
* @param iconPosition [TangemBadgeIconPosition] defining icon position of the badge.
* @param onClick Lambda to be invoked when the badge is clicked (optional).
* @param text TextReference for the badge label.
* @param tangemIconUM Model of representation for the icon to be displayed in the badge.
* @param size [TangemBadgeSize] defining the size of the badge.
* @param shape [TangemBadgeShape] defining the shape of the badge.
* @param color [TangemBadgeColor] defining the color scheme of the badge.
* @param type [TangemBadgeType] defining the style of the badge.
* @param iconPosition [TangemBadgeIconPosition] defining icon position of the badge.
* @param shouldRespectIconTint When true, the icon's own tintReference is preserved instead of being overridden by the
* badge color scheme. Useful when the icon carries its own semantic color (e.g. account icons).
* @param onClick Lambda to be invoked when the badge is clicked (optional).
*/
class TangemBadgeUM(
val text: TextReference,
@ -24,5 +26,6 @@ class TangemBadgeUM(
val color: TangemBadgeColor = TangemBadgeColor.Gray,
val type: TangemBadgeType = TangemBadgeType.Solid,
val iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start,
val shouldRespectIconTint: Boolean = false,
val onClick: (() -> Unit)? = null,
)

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M10,2.5C10.46,2.5 10.833,2.873 10.833,3.333V15.071L16.077,9.827C16.402,9.502 16.93,9.502 17.256,9.827C17.581,10.153 17.581,10.681 17.256,11.006L10.589,17.672L10,18.262L2.744,11.006C2.419,10.68 2.419,10.153 2.744,9.827C3.069,9.502 3.596,9.502 3.922,9.827L9.166,15.071V3.333C9.166,2.873 9.54,2.5 10,2.5Z"
android:fillColor="#ffffff"/>
</vector>