Updated on 2026-08-14
This commit is contained in:
parent
8511623421
commit
7095ceec0f
21 changed files with 717 additions and 183 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
9
core/ui/src/main/res/drawable/ic_arrow_down_20.xml
Normal file
9
core/ui/src/main/res/drawable/ic_arrow_down_20.xml
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue