Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-15 17:20:41 +05:00
commit 866701cdd6
24 changed files with 1568 additions and 371 deletions

View file

@ -1,51 +0,0 @@
package com.tangem.core.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
/**
* Currency placeholder icon
*
* @param id currency id
* @param modifier modifier
*/
@Composable
fun CurrencyPlaceholderIcon(id: String, modifier: Modifier = Modifier) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier.background(color = TangemTheme.colors.icon.secondary, shape = CircleShape),
) {
Text(
text = id.firstOrNull()?.titlecase().orEmpty(),
modifier = Modifier.padding(TangemTheme.dimens.spacing4),
color = TangemTheme.colors.text.primary2,
textAlign = TextAlign.Center,
// FIXME("Incorrect typography. Replace with typography from design system")
style = TangemTheme.typography.h3.copy(fontWeight = FontWeight.Bold),
)
}
}
// region preview
@Preview(showBackground = true, heightDp = 40, widthDp = 40)
@Preview(showBackground = true, heightDp = 40, widthDp = 40, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_CurrencyPlaceholderIcon() {
TangemThemePreview {
CurrencyPlaceholderIcon(id = "DAI")
}
}
// endregion preview

View file

@ -70,17 +70,6 @@ fun BasicDialog(
)
}
@Composable
fun SimpleOkDialog(message: String, onDismissDialog: () -> Unit) {
TangemDialog(
type = DialogType.Message(message),
confirmButton = DialogButtonUM(onClick = onDismissDialog),
onDismissDialog = onDismissDialog,
title = null,
dismissButton = null,
)
}
/**
* Dialog with text field
*
@ -124,34 +113,6 @@ fun TextInputDialog(
)
}
@Composable
fun TextInputDialog(
fieldValue: String,
confirmButton: DialogButtonUM,
onDismissDialog: () -> Unit,
onValueChange: (String) -> Unit,
textFieldParams: AdditionalTextInputDialogUM = remember { AdditionalTextInputDialogUM() },
title: String? = null,
dismissButton: DialogButtonUM? = null,
isDismissable: Boolean = true,
) {
TangemDialog(
type = DialogType.SimpleTextInput(
value = fieldValue,
onValueChange = onValueChange,
params = textFieldParams,
),
confirmButton = confirmButton,
onDismissDialog = onDismissDialog,
title = title,
dismissButton = dismissButton,
properties = DialogProperties(
dismissOnBackPress = isDismissable,
dismissOnClickOutside = isDismissable,
),
)
}
@Composable
fun SelectorDialog(
selectedItemIndex: Int,
@ -441,14 +402,6 @@ private sealed class DialogType {
// endregion Defaults
// region Preview
@Composable
private fun SimpleOkDialogPreview() {
SimpleOkDialog(
message = "All protected passwords will be deleted from the " +
"secure storage, you must enter the wallet password to work with the app.",
) {}
}
@Composable
private fun BasicDialogPreview() {
BasicDialog(
@ -461,15 +414,6 @@ private fun BasicDialogPreview() {
)
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_SimpleOkDialog() {
TangemThemePreview {
SimpleOkDialogPreview()
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable

View file

@ -1,142 +0,0 @@
package com.tangem.core.ui.components
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.TextUnit
import com.tangem.core.ui.components.atoms.text.BoundCounter
/**
* https://stackoverflow.com/questions/69083061/how-to-make-middle-ellipsis-in-text-with-jetpack-compose
*/
@Deprecated("Use EllipsisText with TextEllipsis.Middle ellipsis instead")
@Suppress("LongMethod")
@Composable
fun MiddleEllipsisText(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
softWrap: Boolean = true,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current,
) {
// some letters, like "r", will have less width when placed right before "."
// adding a space to prevent such case
val layoutText = remember(text) { "$text $ellipsisText" }
val textLayoutResultState = remember(layoutText) {
mutableStateOf<TextLayoutResult?>(null)
}
SubcomposeLayout(modifier) { constraints ->
// result is ignored - we only need to fill our textLayoutResult
subcompose("measure") {
Text(
text = layoutText,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
softWrap = softWrap,
maxLines = 1,
onTextLayout = { textLayoutResultState.value = it },
style = style,
)
}.first().measure(Constraints())
// to allow smart cast
val textLayoutResult = textLayoutResultState.value
?: // shouldn't happen - onTextLayout is called before subcompose finishes
return@SubcomposeLayout layout(0, 0) {}
val placeable = subcompose("visible") {
val finalText = remember(text, textLayoutResult, constraints.maxWidth) {
if (
text.isEmpty() ||
textLayoutResult.getBoundingBox(text.indices.last).right <= constraints.maxWidth
) {
// text not including ellipsis fits on the first line.
return@remember text
}
val ellipsisWidth = layoutText.indices.toList()
.takeLast(ELLIPSIS_CHARACTERS_COUNT)
.let widthLet@{ indices ->
// fix this bug: https://issuetracker.google.com/issues/197146630
// in this case width is invalid
for (i in indices) {
val width = textLayoutResult.getBoundingBox(i).width
if (width > 0) {
return@widthLet width * ELLIPSIS_CHARACTERS_COUNT
}
}
// this should not happen, because
// this error occurs only for the last character in the string
error("all ellipsis chars have invalid width")
}
val availableWidth = constraints.maxWidth - ellipsisWidth
val startCounter = BoundCounter(text, textLayoutResult) { it }
val endCounter = BoundCounter(text, textLayoutResult) { text.indices.last - it }
while (availableWidth - startCounter.width - endCounter.width > 0) {
val possibleEndWidth = endCounter.widthWithNextChar()
if (
startCounter.width >= possibleEndWidth &&
availableWidth - startCounter.width - possibleEndWidth >= 0
) {
endCounter.addNextChar()
} else if (availableWidth - startCounter.widthWithNextChar() - endCounter.width >= 0) {
startCounter.addNextChar()
} else {
break
}
}
startCounter.string.trimEnd() + ellipsisText + endCounter.string.reversed().trimStart()
}
Text(
text = finalText,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
softWrap = softWrap,
onTextLayout = onTextLayout,
style = style,
)
}[0].measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
private const val ELLIPSIS_CHARACTERS_COUNT = 3
private const val ELLIPSIS_CHARACTER = '.'
private val ellipsisText = List(ELLIPSIS_CHARACTERS_COUNT) { ELLIPSIS_CHARACTER }.joinToString(separator = "")

View file

@ -1,86 +0,0 @@
package com.tangem.core.ui.components.rows
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH28
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* Simple clickable action row, without input and icon
*
* https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=Ygv5sohTTHYAQcBS-4
*/
@Composable
fun SimpleActionRow(title: String, description: String, modifier: Modifier = Modifier, isClickable: Boolean = true) {
Box(
modifier = modifier
.background(color = TangemTheme.colors.background.action)
.height(TangemTheme.dimens.size44)
.fillMaxWidth(),
) {
Column(
modifier = Modifier
.padding(end = TangemTheme.dimens.spacing48)
.align(Alignment.CenterStart),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
AnimatedContent(targetState = title, label = "") { title ->
Text(
text = title,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
)
}
AnimatedContent(targetState = description, label = "") { description ->
Text(
text = description,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
}
}
if (isClickable) {
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),
contentDescription = null,
modifier = Modifier
.align(alignment = Alignment.CenterEnd)
.padding(end = TangemTheme.dimens.spacing12),
tint = TangemTheme.colors.icon.informative,
)
}
}
}
@Preview
@Composable
private fun SimpleActionRowPreview() {
Column {
TangemThemePreview(isDark = false) {
SimpleActionRow(
title = "Title",
description = "Description",
)
}
SpacerH28()
TangemThemePreview(isDark = false) {
SimpleActionRow(
title = "Title",
description = "Description",
)
}
}
}

View file

@ -1,17 +0,0 @@
package com.tangem.core.ui.components.states
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
data class SelectableItemsState<T>(
val selectedItem: Item<T>,
val items: ImmutableList<Item<T>>,
)
data class Item<T>(
val id: Int,
val startText: TextReference,
val endText: TextReference,
val isSelected: Boolean,
val data: T,
)

View file

@ -29,6 +29,29 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* Tangem badge component to display a small piece of information with optional icon.
* [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8441-83535&m=dev)
*
* @param badgeUM [TangemBadgeUM] containing all the badge parameters.
* @param modifier Modifier to be applied to the badge.
*
[REDACTED_AUTHOR]
*/
@Composable
fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) {
TangemBadge(
text = badgeUM.text,
iconRes = badgeUM.iconRes,
size = badgeUM.size,
shape = badgeUM.shape,
color = badgeUM.color,
type = badgeUM.type,
iconPosition = badgeUM.iconPosition,
modifier = modifier,
)
}
/**
* Tangem badge component to display a small piece of information with optional icon.
* [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8441-83535&m=dev)

View file

@ -0,0 +1,27 @@
package com.tangem.core.ui.ds.badge
import androidx.annotation.DrawableRes
import com.tangem.core.ui.ds.badge.TangemBadgeSize.X9
import com.tangem.core.ui.extensions.TextReference
/**
* UI model for [TangemBadge] component
*
* @param text TextReference for the badge label.
* @param modifier Modifier to be applied to the badge.
* @param iconRes Drawable resource ID 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.
*/
class TangemBadgeUM(
val text: TextReference,
@DrawableRes val iconRes: Int? = null,
val size: TangemBadgeSize = X9,
val shape: TangemBadgeShape = TangemBadgeShape.Default,
val color: TangemBadgeColor = TangemBadgeColor.Gray,
val type: TangemBadgeType = TangemBadgeType.Solid,
val iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start,
)

View file

@ -0,0 +1,189 @@
package com.tangem.core.ui.ds.row
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Constraints
import com.tangem.core.ui.res.TangemTheme
import kotlin.math.max
/**
* A custom layout composable that arranges its children in a row with specific layout IDs.
*/
internal enum class TangemRowLayoutId {
HEAD, START_TOP, END_TOP, START_BOTTOM, END_BOTTOM, TAIL, EXTRA_TOP
}
/**
* A custom layout composable that arranges its children in a row with specific layout IDs.
*
* @param modifier Modifier to be applied to the layout.
* @param contentPadding Padding values to be applied around the content.
* @param content Composable content to be laid out within the TangemRow.
*/
@Suppress("LongMethod")
@Composable
internal fun TangemRowContainer(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(TangemTheme.dimens2.x3),
content: @Composable () -> Unit,
) {
val density = LocalDensity.current
val localDirection = LocalLayoutDirection.current
val verticalPadding = with(density) { TangemTheme.dimens2.x1.roundToPx() }
val contentTopPadding = with(density) { contentPadding.calculateTopPadding().roundToPx() }
val contentBottomPadding = with(density) { contentPadding.calculateBottomPadding().roundToPx() }
val contentStartPadding = with(density) { contentPadding.calculateLeftPadding(localDirection).roundToPx() }
val contentEndPadding = with(density) { contentPadding.calculateRightPadding(localDirection).roundToPx() }
Layout(
content = content,
modifier = modifier,
) { measurables, constraints ->
val layoutWidth = constraints.maxWidth - contentStartPadding - contentEndPadding
val startTopMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt()
val startBottomMinWidth = (layoutWidth * PRICE_MIN_WIDTH_COEFFICIENT).toInt()
// Head composable measurement
val headPlaceable = measurables.measure(
layoutId = TangemRowLayoutId.HEAD,
constraints = constraints,
)
// Tail composable measurement
val tailPlaceable = measurables.measure(
layoutId = TangemRowLayoutId.TAIL,
constraints = constraints,
)
val availableWidthForBody = layoutWidth - headPlaceable.widthOrZero() - tailPlaceable.widthOrZero()
// End top composable width must take the whole free space but is not greater the Start top min size
val endTopPlaceable = measurables.measure(
layoutId = TangemRowLayoutId.END_TOP,
constraints = constraints.copy(
minWidth = 0,
maxWidth = availableWidthForBody - startTopMinWidth,
),
)
// End bottom composable width must take the whole free space but is not greater the Start bottom min size
val endBottomPlaceable = measurables.measure(
layoutId = TangemRowLayoutId.END_BOTTOM,
constraints = constraints.copy(
minWidth = 0,
maxWidth = availableWidthForBody - startBottomMinWidth,
),
)
/* Start top composable will take take the whole REMAINING width space width but no less than minimum */
val startTopPlaceable = measurables.measure(
layoutId = TangemRowLayoutId.START_TOP,
constraints = constraints.copy(
minWidth = startTopMinWidth,
maxWidth = max(
a = startTopMinWidth,
b = availableWidthForBody - endTopPlaceable.widthOrZero(),
),
),
)
/* Start bottom composable will take take the whole REMAINING width space width but no less than minimum */
val startBottomPlaceable = measurables.measure(
layoutId = TangemRowLayoutId.START_BOTTOM,
constraints = constraints.copy(
minWidth = startBottomMinWidth,
maxWidth = max(
a = startBottomMinWidth,
b = availableWidthForBody - endBottomPlaceable.widthOrZero(),
),
),
)
val extraTopPlaceable = measurables.measure(
layoutId = TangemRowLayoutId.EXTRA_TOP,
constraints = constraints,
)
val mainLayoutHeight = maxOf(
headPlaceable.heightOrZero(),
tailPlaceable.heightOrZero(),
startTopPlaceable.heightOrZero() + startBottomPlaceable.heightOrZero() + verticalPadding,
endTopPlaceable.heightOrZero() + endBottomPlaceable.heightOrZero() + verticalPadding,
)
val mainContentTopPadding = if (extraTopPlaceable != null) {
extraTopPlaceable.heightOrZero()
} else {
contentTopPadding
}
val layoutHeight = mainLayoutHeight + mainContentTopPadding + contentBottomPadding
layout(width = constraints.maxWidth, height = layoutHeight) {
extraTopPlaceable?.placeRelative(x = 0, y = 0)
headPlaceable?.placeRelative(
x = contentStartPadding,
y = mainContentTopPadding + (mainLayoutHeight - headPlaceable.height).div(other = 2),
)
startTopPlaceable?.placeRelative(
x = contentStartPadding + headPlaceable.widthOrZero(),
y = mainContentTopPadding + if (startBottomPlaceable == null) {
(mainLayoutHeight - startTopPlaceable.height).div(2)
} else {
0
},
)
startBottomPlaceable?.placeRelative(
x = contentStartPadding + headPlaceable.widthOrZero(),
y = mainContentTopPadding + if (startTopPlaceable == null) {
(mainLayoutHeight - startBottomPlaceable.height).div(2)
} else {
startTopPlaceable.heightOrZero() + verticalPadding
},
)
endTopPlaceable?.placeRelative(
x = layoutWidth - endTopPlaceable.widthOrZero() - tailPlaceable.widthOrZero() + contentEndPadding,
y = mainContentTopPadding + if (endBottomPlaceable == null) {
(mainLayoutHeight - endTopPlaceable.height).div(2)
} else {
0
},
)
endBottomPlaceable?.placeRelative(
x = layoutWidth - endBottomPlaceable.widthOrZero() - tailPlaceable.widthOrZero() + contentEndPadding,
y = mainContentTopPadding + if (endTopPlaceable == null) {
(mainLayoutHeight - endBottomPlaceable.height).div(2)
} else {
endTopPlaceable.heightOrZero() + verticalPadding
},
)
tailPlaceable?.placeRelative(
x = layoutWidth - tailPlaceable.width + contentEndPadding,
y = mainContentTopPadding + (mainLayoutHeight - tailPlaceable.height).div(other = 2),
)
}
}
}
private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.3
private const val PRICE_MIN_WIDTH_COEFFICIENT = 0.32
private fun List<Measurable>.measure(layoutId: TangemRowLayoutId, constraints: Constraints): Placeable? {
return firstOrNull { it.layoutId == layoutId }?.measure(constraints)
}
private fun Placeable?.widthOrZero(): Int = this?.width ?: 0
private fun Placeable?.heightOrZero(): Int = this?.height ?: 0

View file

@ -0,0 +1,168 @@
package com.tangem.core.ui.ds.row.token
import android.content.res.Configuration
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.ds.row.token.internal.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.test.TokenElementsTestTags
import org.burnoutcrew.reorderable.ReorderableLazyListState
/**
* Composable function that represents a Tangem token row in a list.
*
* [Token Row](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8207-17583&t=k8dyaykorsNocGVq-4)
*
* @param tokenRowUM The user model containing the data for the token row.
* @param isBalanceHidden A boolean indicating whether the balance should be hidden.
* @param reorderableTokenListState The state of the reorderable lazy list, if applicable.
* @param modifier The modifier to be applied to the row.
*/
@Composable
fun TangemTokenRow(
tokenRowUM: TangemTokenRowUM,
isBalanceHidden: Boolean,
reorderableTokenListState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
) {
TangemRowContainer(
content = {
CurrencyIcon(
state = tokenRowUM.iconState,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x2)
.testTag(TokenElementsTestTags.TOKEN_ICON),
)
TokenRowPromoBanner(
promoBannerUM = tokenRowUM.promoBannerUM,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.EXTRA_TOP)
.testTag(TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER)
.padding(horizontal = TangemTheme.dimens2.x3)
.fillMaxWidth(),
)
TokenRowTitle(
titleUM = tokenRowUM.titleUM,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.START_TOP)
.padding(end = TangemTheme.dimens2.x2)
.testTag(tag = TokenElementsTestTags.TOKEN_TITLE),
)
TokenRowSubtitle(
subtitleUM = tokenRowUM.subtitleUM,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.START_BOTTOM)
.padding(end = TangemTheme.dimens2.x2)
.testTag(tag = TokenElementsTestTags.TOKEN_PRICE),
)
TokenRowEndTopContent(
endContentUM = tokenRowUM.topEndContentUM,
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.END_TOP)
.testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT),
)
TokenRowEndBottomContent(
endContentUM = tokenRowUM.bottomEndContentUM,
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM)
.testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT),
)
TokenRowTail(
tailUM = tokenRowUM.tailUM,
reorderableTokenListState = reorderableTokenListState,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.TAIL)
.testTag(TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK),
)
},
modifier = modifier
.tokenClickable(tokenRowUM = tokenRowUM),
)
}
@OptIn(ExperimentalFoundationApi::class)
private fun Modifier.tokenClickable(tokenRowUM: TangemTokenRowUM): Modifier = composed {
val hapticFeedback = LocalHapticFeedback.current
val onClick = tokenRowUM.onItemClick
val onLongClick = tokenRowUM.onItemLongClick
val onHapticLongClick = if (onLongClick != null) {
{
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onLongClick(tokenRowUM)
}
} else {
null
}
when {
onClick == null && onLongClick == null -> this
onClick == null && onLongClick != null -> combinedClickable(onClick = {}, onLongClick = onHapticLongClick)
onClick != null && onLongClick == null -> combinedClickable(onClick = { onClick(tokenRowUM) })
onClick != null && onLongClick != null -> {
combinedClickable(onClick = { onClick(tokenRowUM) }, onLongClick = onHapticLongClick)
}
else -> this
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemTokenRow_Preview(
@PreviewParameter(TangemTokenRowPreviewProvider::class) tokenRowUM: TangemTokenRowUM,
) {
TangemThemePreviewRedesign {
TangemTokenRow(
tokenRowUM = tokenRowUM,
isBalanceHidden = false,
reorderableTokenListState = null,
modifier = Modifier.background(TangemTheme.colors2.surface.level1),
)
}
}
private class TangemTokenRowPreviewProvider : CollectionPreviewParameterProvider<TangemTokenRowUM>(
collection = listOf(
TangemTokenRowPreviewData.defaultState,
TangemTokenRowPreviewData.defaultEllipsisState,
TangemTokenRowPreviewData.tokenState,
TangemTokenRowPreviewData.customTokenState,
TangemTokenRowPreviewData.draggableState,
TangemTokenRowPreviewData.draggableStateV2,
TangemTokenRowPreviewData.loadingState,
TangemTokenRowPreviewData.unreachableState,
TangemTokenRowPreviewData.accountState,
TangemTokenRowPreviewData.accountLetterState,
TangemTokenRowPreviewData.accountEllipsisState,
TangemTokenRowPreviewData.promoBannerState,
),
)
// endregion

View file

@ -0,0 +1,172 @@
package com.tangem.core.ui.ds.row.token
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.ds.badge.TangemBadgeUM
import com.tangem.core.ui.extensions.ColorReference2
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Immutable
sealed class TangemTokenRowUM {
/** Unique id */
abstract val id: String
/** Token icon state */
abstract val iconState: CurrencyIconState
/** Token title UM (in one row with [topEndContentUM]) */
abstract val titleUM: TitleUM
/** Token subtitle UM (in one row with [bottomEndContentUM]) */
abstract val subtitleUM: SubtitleUM
/** Top end content UM (e.i. fiat amount in one row with [titleUM]) */
abstract val topEndContentUM: EndContentUM
/** Bottom end content UM (e.i. crypto amount in one row with [subtitleUM]) */
abstract val bottomEndContentUM: EndContentUM
/** Token row tail UM */
abstract val tailUM: TailUM
/** Promo banner UM */
abstract val promoBannerUM: PromoBannerUM
/** Callback which will be called when an item is clicked */
abstract val onItemClick: ((TangemTokenRowUM) -> Unit)?
/** Callback which will be called when an item is long clicked */
abstract val onItemLongClick: ((TangemTokenRowUM) -> Unit)?
/**
* Content state of [TangemTokenRowUM]
*/
data class Content(
override val id: String,
override val iconState: CurrencyIconState,
override val titleUM: TitleUM,
override val subtitleUM: SubtitleUM,
override val topEndContentUM: EndContentUM,
override val bottomEndContentUM: EndContentUM,
override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty,
override val tailUM: TailUM = TailUM.Empty,
override val onItemClick: ((TangemTokenRowUM) -> Unit)?,
override val onItemLongClick: ((TangemTokenRowUM) -> Unit)?,
) : TangemTokenRowUM()
/**
* Loading state of [TangemTokenRowUM]
*/
data class Loading(
override val id: String,
override val iconState: CurrencyIconState,
override val titleUM: TitleUM = TitleUM.Loading,
override val subtitleUM: SubtitleUM = SubtitleUM.Loading,
) : TangemTokenRowUM() {
override val topEndContentUM: EndContentUM = EndContentUM.Loading
override val bottomEndContentUM: EndContentUM = EndContentUM.Loading
override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty
override val tailUM: TailUM = TailUM.Empty
override val onItemClick: ((TangemTokenRowUM) -> Unit)? = null
override val onItemLongClick: ((TangemTokenRowUM) -> Unit)? = null
}
/**
* Actionable state of [TangemTokenRowUM]
*/
data class Actionable(
override val id: String,
override val iconState: CurrencyIconState,
override val titleUM: TitleUM,
override val subtitleUM: SubtitleUM,
override val tailUM: TailUM,
override val onItemClick: ((TangemTokenRowUM) -> Unit)?,
override val onItemLongClick: ((TangemTokenRowUM) -> Unit)?,
override val topEndContentUM: EndContentUM = EndContentUM.Empty,
override val bottomEndContentUM: EndContentUM = EndContentUM.Empty,
) : TangemTokenRowUM() {
override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty
}
@Immutable
sealed class TitleUM {
data class Content(
val text: TextReference,
val hasPending: Boolean = false,
val isAvailable: Boolean = true,
val badge: TangemBadgeUM? = null,
val onBadgeClick: (() -> Unit)? = null,
) : TitleUM()
data object Loading : TitleUM()
data object Empty : TitleUM()
}
@Immutable
sealed class SubtitleUM {
data class Content(
val text: TextReference,
val isAvailable: Boolean = true,
val isFlickering: Boolean = false,
val icons: ImmutableList<IconUM> = persistentListOf(),
val priceChangeUM: PriceChangeState = PriceChangeState.Unknown,
val badge: TangemBadgeUM? = null,
) : SubtitleUM()
data object Loading : SubtitleUM()
data object Empty : SubtitleUM()
}
@Immutable
sealed class EndContentUM {
data class Content(
val text: TextReference,
val isAvailable: Boolean = true,
val isFlickering: Boolean = false,
val icons: ImmutableList<IconUM> = persistentListOf(),
val priceChangeUM: PriceChangeState = PriceChangeState.Unknown,
) : EndContentUM()
data object Loading : EndContentUM()
data object Empty : EndContentUM()
}
@Immutable
sealed class PromoBannerUM {
data class Content(
val title: TextReference,
val onPromoBannerClick: () -> Unit,
val onCloseClick: () -> Unit,
val onPromoShown: () -> Unit = {},
) : PromoBannerUM()
data object Empty : PromoBannerUM()
}
@Immutable
sealed class TailUM {
data class Text(
val text: TextReference,
) : TailUM()
data object Draggable : TailUM()
data object Empty : TailUM()
}
data class IconUM(
val iconRes: Int,
val tintReference: ColorReference2 = ColorReference2 { TangemTheme.colors2.graphic.neutral.primary },
)
}

View file

@ -0,0 +1,266 @@
package com.tangem.core.ui.ds.row.token.internal
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import com.tangem.core.ui.R
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.styledResourceReference
import com.tangem.core.ui.extensions.styledStringReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.persistentListOf
import java.util.UUID
internal object TangemTokenRowPreviewData {
private val priceChangeState: PriceChangeState.Content
get() = PriceChangeState.Content(
type = PriceChangeType.UP,
valueInPercent = "2.45%",
)
val promoBannerUM: TangemTokenRowUM.PromoBannerUM.Content
get() = TangemTokenRowUM.PromoBannerUM.Content(
title = stringReference("Earn yield by supplying your crypto assets"),
onPromoBannerClick = {},
onPromoShown = {},
onCloseClick = {},
)
val titleUM: TangemTokenRowUM.TitleUM.Content
get() = TangemTokenRowUM.TitleUM.Content(
text = stringReference(value = "Polygon"),
hasPending = true,
)
val subtitleUM: TangemTokenRowUM.SubtitleUM.Content
get() = TangemTokenRowUM.SubtitleUM.Content(
text = stringReference(value = "$ 0.6631"),
priceChangeUM = priceChangeState,
)
val topEndContentUM: TangemTokenRowUM.EndContentUM.Content
get() = TangemTokenRowUM.EndContentUM.Content(
text = combinedReference(
stringReference("$ 500"),
styledStringReference(".17", {
SpanStyle(
color = TangemTheme.colors2.text.neutral.secondary,
fontWeight = TangemTheme.typography2.bodyRegular16.fontWeight,
)
}),
),
)
val bottomEndContentUM: TangemTokenRowUM.EndContentUM.Content
get() = TangemTokenRowUM.EndContentUM.Content(
text = stringReference("Title"),
)
private val accountResIcon: CurrencyIconState.CryptoPortfolio.Icon
get() = CurrencyIconState.CryptoPortfolio.Icon(
resId = R.drawable.ic_rounded_star_24,
color = Color.Blue,
isGrayscale = false,
)
private val accountLetterIcon: CurrencyIconState.CryptoPortfolio.Letter
get() = CurrencyIconState.CryptoPortfolio.Letter(
char = stringReference("A"),
color = Color.Blue,
isGrayscale = false,
)
private val coinIconState
get() = CurrencyIconState.CoinIcon(
url = null,
fallbackResId = R.drawable.img_polygon_22,
isGrayscale = false,
shouldShowCustomBadge = false,
)
private val tokenIconState
get() = CurrencyIconState.TokenIcon(
url = null,
topBadgeIconResId = R.drawable.img_polygon_22,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
shouldShowCustomBadge = false,
)
private val customTokenIconState
get() = CurrencyIconState.CustomTokenIcon(
tint = TangemColorPalette.Black,
background = TangemColorPalette.Meadow,
topBadgeIconResId = R.drawable.img_polygon_22,
isGrayscale = false,
)
val defaultState: TangemTokenRowUM.Content
get() = TangemTokenRowUM.Content(
id = UUID.randomUUID().toString(),
iconState = coinIconState,
titleUM = titleUM,
subtitleUM = subtitleUM,
topEndContentUM = topEndContentUM,
bottomEndContentUM = bottomEndContentUM,
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
tailUM = TangemTokenRowUM.TailUM.Empty,
onItemClick = {},
onItemLongClick = {},
)
val defaultEllipsisState: TangemTokenRowUM.Content
get() = defaultState.copy(
titleUM = titleUM.copy(text = stringReference("Polygon Polygon Polygon Polygon")),
subtitleUM = subtitleUM,
topEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = combinedReference(
stringReference("$ 500"),
styledStringReference(".11232131237", {
SpanStyle(
color = TangemTheme.colors2.text.neutral.secondary,
fontWeight = TangemTheme.typography2.bodyRegular16.fontWeight,
)
}),
),
icons = persistentListOf(
TangemTokenRowUM.IconUM(R.drawable.ic_staking_mini_10),
TangemTokenRowUM.IconUM(R.drawable.ic_attention_12),
TangemTokenRowUM.IconUM(R.drawable.ic_error_sync_24),
),
),
bottomEndContentUM = bottomEndContentUM,
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
tailUM = TangemTokenRowUM.TailUM.Empty,
onItemClick = {},
onItemLongClick = {},
)
val tokenState: TangemTokenRowUM.Content
get() = TangemTokenRowUM.Content(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleUM = titleUM,
subtitleUM = subtitleUM,
topEndContentUM = topEndContentUM,
bottomEndContentUM = bottomEndContentUM,
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
tailUM = TangemTokenRowUM.TailUM.Empty,
onItemClick = {},
onItemLongClick = {},
)
val customTokenState: TangemTokenRowUM.Content
get() = TangemTokenRowUM.Content(
id = UUID.randomUUID().toString(),
iconState = customTokenIconState,
titleUM = titleUM,
subtitleUM = subtitleUM,
topEndContentUM = topEndContentUM,
bottomEndContentUM = bottomEndContentUM,
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
tailUM = TangemTokenRowUM.TailUM.Empty,
onItemClick = {},
onItemLongClick = {},
)
val draggableState: TangemTokenRowUM.Actionable
get() = TangemTokenRowUM.Actionable(
id = UUID.randomUUID().toString(),
iconState = coinIconState,
titleUM = titleUM,
subtitleUM = subtitleUM,
tailUM = TangemTokenRowUM.TailUM.Draggable,
onItemClick = {},
onItemLongClick = {},
)
val draggableStateV2: TangemTokenRowUM.Actionable
get() = TangemTokenRowUM.Actionable(
id = UUID.randomUUID().toString(),
iconState = coinIconState,
titleUM = titleUM,
subtitleUM = subtitleUM,
topEndContentUM = topEndContentUM,
bottomEndContentUM = bottomEndContentUM,
tailUM = TangemTokenRowUM.TailUM.Draggable,
onItemClick = {},
onItemLongClick = {},
)
val loadingState: TangemTokenRowUM.Loading
get() = TangemTokenRowUM.Loading(
id = UUID.randomUUID().toString(),
iconState = coinIconState,
titleUM = TangemTokenRowUM.TitleUM.Loading,
subtitleUM = TangemTokenRowUM.SubtitleUM.Loading,
)
val unreachableState: TangemTokenRowUM.Content
get() = defaultState.copy(
topEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = stringReference(StringsSigns.DASH_SIGN),
),
bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = styledResourceReference(
id = R.string.common_unreachable,
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) },
),
),
)
val accountState: TangemTokenRowUM.Content
get() = TangemTokenRowUM.Content(
id = UUID.randomUUID().toString(),
iconState = accountResIcon,
titleUM = TangemTokenRowUM.TitleUM.Content(
text = stringReference(value = "Portfolio"),
),
subtitleUM = TangemTokenRowUM.SubtitleUM.Content(
text = stringReference("24 tokens"),
),
topEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = stringReference("22,129.65 $"),
),
bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = stringReference("+ $ 1,245.32"),
priceChangeUM = priceChangeState,
),
onItemClick = {},
onItemLongClick = {},
)
val accountLetterState: TangemTokenRowUM.Content
get() = accountState.copy(
iconState = accountLetterIcon,
)
val accountEllipsisState: TangemTokenRowUM.Content
get() = accountState.copy(
titleUM = TangemTokenRowUM.TitleUM.Content(
text = stringReference(value = "Portfolio Portfolio Portfolio Portfolio"),
),
subtitleUM = TangemTokenRowUM.SubtitleUM.Content(
text = stringReference("24 tokens 24 tokens 24 tokens 24 tokens"),
),
topEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = stringReference("22,129.6129387147653025 $"),
),
bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = stringReference("+ $ 1,245.31093284302752"),
priceChangeUM = priceChangeState,
),
)
val promoBannerState: TangemTokenRowUM.Content
get() = defaultState.copy(
promoBannerUM = promoBannerUM,
)
}

View file

@ -0,0 +1,100 @@
package com.tangem.core.ui.ds.row.token.internal
import android.content.res.Configuration
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
internal fun TokenRowEndBottomContent(
endContentUM: TangemTokenRowUM.EndContentUM,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
when (endContentUM) {
is TangemTokenRowUM.EndContentUM.Content -> Content(
modifier = modifier,
endContentUM = endContentUM,
isBalanceHidden = isBalanceHidden,
)
TangemTokenRowUM.EndContentUM.Empty -> Unit
TangemTokenRowUM.EndContentUM.Loading -> TextShimmer(
style = TangemTheme.typography2.captionSemibold12,
modifier = modifier.width(TangemTheme.dimens2.x10),
radius = TangemTheme.dimens2.x25,
)
}
}
@Composable
private fun Content(
endContentUM: TangemTokenRowUM.EndContentUM.Content,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography2.captionSemibold12.applyBladeBrush(
isEnabled = endContentUM.isFlickering,
textColor = if (endContentUM.isAvailable) {
TangemTheme.colors2.text.neutral.secondary
} else {
TangemTheme.colors2.text.status.disabled
},
),
)
when (endContentUM.priceChangeUM) {
is PriceChangeState.Content -> TokenRowPriceChangeContent(
priceChangeState = endContentUM.priceChangeUM,
isFlickering = endContentUM.isFlickering,
isAvailable = endContentUM.isAvailable,
)
PriceChangeState.Unknown -> Unit
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TokenRowEndBottomContent_Preview(
@PreviewParameter(TokenRowEndBottomContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM,
) {
TangemThemePreviewRedesign {
TokenRowEndBottomContent(
endContentUM = params,
isBalanceHidden = false,
)
}
}
private class TokenRowEndBottomContentPreviewProvider : PreviewParameterProvider<TangemTokenRowUM.EndContentUM> {
override val values: Sequence<TangemTokenRowUM.EndContentUM>
get() = sequenceOf(
TangemTokenRowPreviewData.bottomEndContentUM,
)
}
// endregion

View file

@ -0,0 +1,115 @@
package com.tangem.core.ui.ds.row.token.internal
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.TextOverflow
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.util.fastForEach
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
internal fun TokenRowEndTopContent(
endContentUM: TangemTokenRowUM.EndContentUM,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
when (endContentUM) {
is TangemTokenRowUM.EndContentUM.Content -> Content(
modifier = modifier,
endContentUM = endContentUM,
isBalanceHidden = isBalanceHidden,
)
TangemTokenRowUM.EndContentUM.Empty -> Unit
TangemTokenRowUM.EndContentUM.Loading -> TextShimmer(
style = TangemTheme.typography2.bodySemibold16,
modifier = modifier.width(TangemTheme.dimens2.x18),
radius = TangemTheme.dimens2.x25,
)
}
}
@Composable
private fun Content(
endContentUM: TangemTokenRowUM.EndContentUM.Content,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedVisibility(
visible = endContentUM.icons.isNotEmpty(),
) {
Row(
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x1),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
) {
endContentUM.icons.fastForEach { icon ->
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x3),
painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)),
tint = icon.tintReference(),
contentDescription = null,
)
}
}
}
Text(
modifier = Modifier,
text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography2.bodySemibold16.applyBladeBrush(
isEnabled = endContentUM.isFlickering,
textColor = if (endContentUM.isAvailable) {
TangemTheme.colors2.text.neutral.primary
} else {
TangemTheme.colors2.text.status.disabled
},
),
)
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TokenRowEndTopContent_Preview(
@PreviewParameter(TokenRowEndContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM,
) {
TangemThemePreviewRedesign {
TokenRowEndTopContent(
endContentUM = params,
isBalanceHidden = false,
)
}
}
private class TokenRowEndContentPreviewProvider : PreviewParameterProvider<TangemTokenRowUM.EndContentUM> {
override val values: Sequence<TangemTokenRowUM.EndContentUM>
get() = sequenceOf(
TangemTokenRowPreviewData.topEndContentUM,
)
}
// endregion

View file

@ -0,0 +1,71 @@
package com.tangem.core.ui.ds.row.token.internal
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun RowScope.TokenRowPriceChangeContent(
priceChangeState: PriceChangeState.Content,
isFlickering: Boolean,
isAvailable: Boolean = true,
) {
AnimatedContent(
targetState = priceChangeState.type,
label = "Update the price change's arrow",
modifier = Modifier.padding(start = TangemTheme.dimens2.x1),
) { animatedType ->
Icon(
painter = rememberVectorPainter(
ImageVector.vectorResource(
when (animatedType) {
PriceChangeType.UP -> R.drawable.ic_up_dynamic_24
PriceChangeType.DOWN -> R.drawable.ic_down_dynamic_24
PriceChangeType.NEUTRAL -> R.drawable.ic_static_dynamic_24
},
),
),
tint = when (animatedType) {
PriceChangeType.UP -> TangemTheme.colors2.graphic.status.accent
PriceChangeType.DOWN -> TangemTheme.colors2.graphic.status.warning
PriceChangeType.NEUTRAL -> TangemTheme.colors2.graphic.neutral.secondary
},
contentDescription = null,
modifier = Modifier.size(TangemTheme.dimens2.x3),
)
}
AnimatedContent(
targetState = priceChangeState.valueInPercent,
label = "Update the price text",
modifier = Modifier.padding(start = TangemTheme.dimens2.x0_5),
) { animatedText ->
Text(
text = animatedText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography2.captionSemibold12.applyBladeBrush(
isEnabled = isFlickering,
textColor = if (isAvailable) {
TangemTheme.colors2.text.neutral.secondary
} else {
TangemTheme.colors2.text.status.disabled
},
),
)
}
}

View file

@ -0,0 +1,107 @@
package com.tangem.core.ui.ds.row.token.internal
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
internal fun TokenRowPromoBanner(promoBannerUM: TangemTokenRowUM.PromoBannerUM, modifier: Modifier = Modifier) {
when (promoBannerUM) {
is TangemTokenRowUM.PromoBannerUM.Content -> TokenRowPromoBanner(
promoBannerUM = promoBannerUM,
modifier = modifier,
)
TangemTokenRowUM.PromoBannerUM.Empty -> Unit
}
}
@Composable
internal fun TokenRowPromoBanner(promoBannerUM: TangemTokenRowUM.PromoBannerUM.Content, modifier: Modifier = Modifier) {
LaunchedEffect(promoBannerUM) {
promoBannerUM.onPromoShown()
}
val bgColor = TangemTheme.colors.control.default
Column(modifier = modifier) {
Row(
modifier = Modifier
.background(color = bgColor, shape = RoundedCornerShape(TangemTheme.dimens2.x4))
.clickable(onClick = promoBannerUM.onPromoBannerClick)
.padding(horizontal = TangemTheme.dimens2.x3, vertical = TangemTheme.dimens2.x2)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_analytics_up_24),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
modifier = Modifier
.padding(end = TangemTheme.dimens2.x2)
.size(TangemTheme.dimens2.x4),
)
Text(
text = promoBannerUM.title.resolveReference(),
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.secondary,
modifier = Modifier
.weight(1f)
.padding(end = TangemTheme.dimens2.x2),
)
Icon(
painter = painterResource(id = R.drawable.ic_close_24),
contentDescription = null,
tint = TangemTheme.colors2.text.neutral.secondary,
modifier = Modifier
.size(TangemTheme.dimens2.x4)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = false),
onClick = { promoBannerUM.onCloseClick() },
),
)
}
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_rectangle_bottom),
contentDescription = null,
tint = bgColor,
modifier = Modifier
.size(width = TangemTheme.dimens2.x3, height = TangemTheme.dimens2.x2),
)
}
}
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_TokenRowPromoBanner() {
TangemThemePreviewRedesign {
TokenRowPromoBanner(
promoBannerUM = TangemTokenRowPreviewData.promoBannerUM,
modifier = Modifier.fillMaxWidth(),
)
}
}

View file

@ -0,0 +1,100 @@
package com.tangem.core.ui.ds.row.token.internal
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.ds.badge.TangemBadge
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
internal fun TokenRowSubtitle(subtitleUM: TangemTokenRowUM.SubtitleUM, modifier: Modifier = Modifier) {
when (subtitleUM) {
is TangemTokenRowUM.SubtitleUM.Content -> SubtitleContent(
subtitleUM = subtitleUM,
modifier = modifier,
)
TangemTokenRowUM.SubtitleUM.Loading -> TextShimmer(
style = TangemTheme.typography2.captionSemibold12,
modifier = modifier.width(TangemTheme.dimens2.x8),
radius = TangemTheme.dimens2.x25,
)
TangemTokenRowUM.SubtitleUM.Empty -> Unit
}
}
@Composable
private fun SubtitleContent(subtitleUM: TangemTokenRowUM.SubtitleUM.Content, modifier: Modifier = Modifier) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
AnimatedVisibility(
visible = subtitleUM.badge != null,
) {
val wrappedBadge = remember(this) { requireNotNull(subtitleUM.badge) }
TangemBadge(wrappedBadge)
}
Text(
text = subtitleUM.text.resolveAnnotatedReference(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography2.captionSemibold12.applyBladeBrush(
isEnabled = subtitleUM.isFlickering,
textColor = if (subtitleUM.isAvailable) {
TangemTheme.colors2.text.neutral.secondary
} else {
TangemTheme.colors2.text.status.disabled
},
),
)
when (subtitleUM.priceChangeUM) {
is PriceChangeState.Content -> TokenRowPriceChangeContent(
priceChangeState = subtitleUM.priceChangeUM,
isFlickering = subtitleUM.isFlickering,
isAvailable = subtitleUM.isAvailable,
)
PriceChangeState.Unknown -> Unit
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TokenRowSubtitle_Preview(
@PreviewParameter(TokenRowSubtitlePreviewProvider::class) params: TangemTokenRowUM.SubtitleUM,
) {
TangemThemePreviewRedesign {
TokenRowSubtitle(
subtitleUM = params,
)
}
}
private class TokenRowSubtitlePreviewProvider : PreviewParameterProvider<TangemTokenRowUM.SubtitleUM> {
override val values: Sequence<TangemTokenRowUM.SubtitleUM>
get() = sequenceOf(
TangemTokenRowPreviewData.subtitleUM,
TangemTokenRowUM.SubtitleUM.Loading,
)
}
// endregion

View file

@ -0,0 +1,81 @@
package com.tangem.core.ui.ds.row.token.internal
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.OrganizeTokensScreenTestTags
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder
@Composable
internal fun TokenRowTail(
tailUM: TangemTokenRowUM.TailUM,
reorderableTokenListState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
) {
AnimatedContent(
targetState = tailUM,
label = "Update non content fiat block",
modifier = modifier,
contentKey = { it::class.java },
) { animatedState ->
val innerModifier = Modifier.padding(start = TangemTheme.dimens2.x2)
when (animatedState) {
TangemTokenRowUM.TailUM.Empty -> Unit
is TangemTokenRowUM.TailUM.Draggable -> DraggableImage(
reorderableTokenListState = reorderableTokenListState,
modifier = innerModifier,
)
is TangemTokenRowUM.TailUM.Text -> ContentText(text = animatedState.text, modifier = innerModifier)
}
}
}
@Composable
private fun DraggableImage(reorderableTokenListState: ReorderableLazyListState?, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(size = TangemTheme.dimens2.x6)
.then(
other = if (reorderableTokenListState != null) {
Modifier.detectReorder(reorderableTokenListState)
} else {
Modifier
},
)
.testTag(OrganizeTokensScreenTestTags.DRAGGABLE_IMAGE),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_drag_24),
tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant,
contentDescription = null,
)
}
}
@Composable
private fun ContentText(text: TextReference, modifier: Modifier = Modifier) {
Text(
text = text.resolveAnnotatedReference(),
color = TangemTheme.colors2.text.neutral.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography2.captionRegular12,
modifier = modifier,
)
}

View file

@ -0,0 +1,101 @@
package com.tangem.core.ui.ds.row.token.internal
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.ds.badge.TangemBadge
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
internal fun TokenRowTitle(titleUM: TangemTokenRowUM.TitleUM, modifier: Modifier = Modifier) {
when (titleUM) {
is TangemTokenRowUM.TitleUM.Content -> ContentTitle(titleUM = titleUM, modifier = modifier)
TangemTokenRowUM.TitleUM.Loading -> TextShimmer(
style = TangemTheme.typography2.bodySemibold16,
modifier = modifier.width(TangemTheme.dimens2.x18),
radius = TangemTheme.dimens2.x25,
)
TangemTokenRowUM.TitleUM.Empty -> Unit
}
}
@Composable
private fun ContentTitle(titleUM: TangemTokenRowUM.TitleUM.Content, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens2.x4),
verticalAlignment = Alignment.CenterVertically,
) {
/*
* If currency name has a long width, then it will completely displace the image.
* So we need to use [weight] to avoid displacement.
*/
Text(
text = titleUM.text.resolveReference(),
modifier = Modifier.weight(weight = 1f, fill = false),
color = if (titleUM.isAvailable) {
TangemTheme.colors2.text.neutral.primary
} else {
TangemTheme.colors2.text.status.disabled
},
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography2.bodySemibold16,
)
AnimatedVisibility(
visible = titleUM.hasPending,
modifier = Modifier.align(alignment = Alignment.CenterVertically),
) {
Image(
painter = painterResource(id = R.drawable.img_loader_15),
contentDescription = null,
)
}
AnimatedVisibility(
visible = titleUM.badge != null,
modifier = Modifier.conditional(
condition = titleUM.onBadgeClick != null,
modifier = { clickable(onClick = requireNotNull(titleUM.onBadgeClick)) },
),
) {
val wrappedBadge = remember(this) { requireNotNull(titleUM.badge) }
TangemBadge(wrappedBadge)
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TokenRowTitle_Preview() {
TangemThemePreviewRedesign {
TokenRowTitle(
titleUM = TangemTokenRowPreviewData.titleUM,
modifier = Modifier.fillMaxWidth(),
)
}
}
// endregion

View file

@ -2,6 +2,8 @@ package com.tangem.core.ui.extensions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.Color
/**
@ -11,7 +13,7 @@ import androidx.compose.ui.graphics.Color
*
* @property value color provider from theme
*/
@Deprecated("Use TextReference with applied SpanStyleReference for colored text.")
@Deprecated("Use TextReference with applied SpanStyleReference for colored text or ColorReference2 for color.")
@Immutable
data class ColorReference(val value: @Composable () -> Color)
@ -31,4 +33,17 @@ fun themedColor(value: @Composable () -> Color): ColorReference {
@Composable
fun ColorReference.resolveReference(): Color {
return value()
}
/**
* Utility functional interface for keeping themed [Color] reference from app theme.
* It is necessary to use [Stable] annotation for runtime stability.
*/
@Stable
@FunctionalInterface
fun interface ColorReference2 {
@ReadOnlyComposable
@Composable
operator fun invoke(): Color
}

View file

@ -1,15 +0,0 @@
package com.tangem.core.ui.utils
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
fun Modifier.disableNestedScroll(): Modifier = nestedScroll(DisableParentConnection)
private object DisableParentConnection : NestedScrollConnection {
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
return available.copy(x = 0f)
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M5.28,4C4.294,4 3.679,5.064 4.176,5.911L10.896,17.369C11.389,18.21 12.611,18.21 13.104,17.369L19.824,5.911C20.321,5.064 19.706,4 18.72,4H5.28Z"
android:fillColor="#000000"/>
</vector>

View file

@ -4,7 +4,9 @@
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19,8H5V10H19V8ZM19,14H5V16H19V14Z"
android:fillColor="#B0B0B0"
android:fillType="evenOdd"/>
android:pathData="M5,8H19M5,16H19"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,12m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"
android:fillColor="#000000"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M5.28,20C4.294,20 3.679,18.936 4.176,18.089L10.896,6.631C11.389,5.79 12.611,5.79 13.104,6.631L19.824,18.089C20.321,18.936 19.706,20 18.72,20H5.28Z"
android:fillColor="#000000"/>
</vector>