diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt index 943fb98735..1955e4b9a0 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt @@ -47,7 +47,7 @@ class AccountCryptoPortfolioItemStateConverter( ) return TokenItemState.Content( id = account.accountId.toItemId(), - iconState = AccountIconItemStateConverter.convert(this), + iconState = AccountIconItemStateConverter().convert(this), titleState = TokenItemState.TitleState.Content( text = accountName.toUM().value, ), @@ -73,7 +73,7 @@ class AccountCryptoPortfolioItemStateConverter( private fun Account.CryptoPortfolio.mapToLoadingState(): TokenItemState.Content { return TokenItemState.Content( id = account.accountId.toItemId(), - iconState = AccountIconItemStateConverter.convert(account), + iconState = AccountIconItemStateConverter().convert(account), titleState = TokenItemState.TitleState.Content( text = accountName.toUM().value, ), @@ -95,7 +95,7 @@ class AccountCryptoPortfolioItemStateConverter( private fun Account.CryptoPortfolio.mapToUnreachableState(): TokenItemState.Unreachable { return TokenItemState.Unreachable( id = account.accountId.toItemId(), - iconState = AccountIconItemStateConverter.convert(account), + iconState = AccountIconItemStateConverter().convert(account), titleState = TokenItemState.TitleState.Content( text = accountName.toUM().value, ), diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt index 102be5a837..6cc4b2bf7e 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt @@ -1,11 +1,14 @@ package com.tangem.common.ui.account +import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.utils.converter.Converter -object AccountIconItemStateConverter : Converter { +class AccountIconItemStateConverter( + val size: AccountIconSize = AccountIconSize.Default, +) : Converter { override fun convert(value: Account): CurrencyIconState.CryptoPortfolio = when (value) { is Account.CryptoPortfolio -> when { @@ -13,11 +16,13 @@ object AccountIconItemStateConverter : Converter CurrencyIconState.CryptoPortfolio.Icon( resId = value.icon.value.getResId(), color = value.icon.color.getUiColor(), isGrayscale = false, + size = size, ) } is Account.Payment -> TODO("[REDACTED_JIRA]") diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt index 7f8ec96750..1c3870ab40 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt @@ -1,17 +1,35 @@ package com.tangem.common.ui.notifications +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +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 androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.ds.TangemPagerIndicator import com.tangem.core.ui.ds.message.TangemMessage +import com.tangem.core.ui.ds.message.TangemMessageEffect import com.tangem.core.ui.ds.message.TangemMessageUM +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf fun LazyListScope.notifications( notifications: ImmutableList, @@ -110,4 +128,103 @@ fun LazyListScope.notifications( ) }, ) -} \ No newline at end of file +} + +/** + * Displays a list of notifications in a stacked manner using a HorizontalPager. + * If there are multiple notifications, a PagerIndicator is shown below the notifications. + * + * @param notifications List of TangemMessageUM objects to be displayed. + * @param containerColor Color to be used for the background of the notifications. + * @param modifier Optional Modifier for the notifications. + */ +fun LazyListScope.stackedNotifications( + notifications: ImmutableList?, + containerColor: Color, + modifier: Modifier = Modifier, +) { + item { + if (!notifications.isNullOrEmpty()) { + val notificationsPagerState = rememberPagerState( + pageCount = { notifications.size }, + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = TangemTheme.dimens2.x2), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), + ) { + HorizontalPager( + state = notificationsPagerState, + modifier = Modifier + .fillMaxSize() + .animateItem(null, null, null), + ) { page -> + TangemMessage( + messageUM = notifications[page], + contentColor = containerColor, + modifier = modifier, + ) + } + if (notifications.size > 1) { + TangemPagerIndicator( + pagerState = notificationsPagerState, + ) + } + } + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun StackedNotifications_Preview( + @PreviewParameter(StackedNotificationsPreviewProvider::class) params: ImmutableList, +) { + TangemThemePreviewRedesign { + val contentColor = TangemTheme.colors2.surface.level1 + LazyColumn( + modifier = Modifier + .background(contentColor) + .padding(16.dp), + ) { + stackedNotifications( + notifications = params, + containerColor = contentColor, + ) + } + } +} + +private class StackedNotificationsPreviewProvider : PreviewParameterProvider> { + override val values: Sequence> + get() = sequenceOf( + persistentListOf( + TangemMessageUM( + id = "0", + title = stringReference("First notification"), + subtitle = stringReference("This is the first notification"), + messageEffect = TangemMessageEffect.Magic, + ), + ), + persistentListOf( + TangemMessageUM( + id = "0", + title = stringReference("First notification"), + subtitle = stringReference("This is the first notification"), + messageEffect = TangemMessageEffect.Magic, + ), + TangemMessageUM( + id = "1", + title = stringReference("Second notification"), + subtitle = stringReference("This is the second notification"), + messageEffect = TangemMessageEffect.Card, + ), + ), + ) +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt new file mode 100644 index 0000000000..5528190513 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt @@ -0,0 +1,370 @@ +package com.tangem.core.ui.ds + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlin.math.abs +import kotlin.math.min +import kotlin.math.roundToInt + +private const val ANIMATION_DURATION = 300 +private const val MAX_VISIBLE_DOTS = 5 +private const val MIN_HIDDEN_FOR_SMALL_DOT = 2 +private const val MIN_DISTANCE_FOR_SMALL_DOT = 3 +private const val MIN_DISTANCE_FOR_HINT_DOT = 2 + +private val SPACING = 4.dp +private val CURRENT_DOT_SIZE = DpSize(16.dp, 8.dp) +private val NORMAL_DOT_SIZE = DpSize(8.dp, 8.dp) +private val HINT_DOT_SIZE = DpSize(6.dp, 6.dp) +private val SMALL_DOT_SIZE = DpSize(4.dp, 4.dp) + +/** + * // TODO Cleanup and document this code, it's quite complex and has some "magic numbers" that need explanation. + * + * A pager indicator that adapts to the number of pages and the current page index. + * + * For 5 or fewer pages, it shows all dots with the current page highlighted. + * For more than 5 pages, it shows a sliding window of 5 dots with size and opacity indicating position. + * + * @param pagerState state of the pager to observe + * @param activeIndicatorColor color for the active page indicator + * @param inactiveIndicatorColor color for the inactive page indicators + * @param modifier modifier for styling + */ +@Suppress("LongMethod", "CyclomaticComplexMethod") +@Composable +fun TangemPagerIndicator( + pagerState: PagerState, + modifier: Modifier = Modifier, + activeIndicatorColor: Color = TangemTheme.colors2.graphic.neutral.primary, + inactiveIndicatorColor: Color = TangemTheme.colors2.graphic.neutral.tertiary, +) { + val totalPages = pagerState.pageCount + val currentIndex = pagerState.currentPage + + if (totalPages == 0) return + + val density = LocalDensity.current + + val (targetLower, targetUpper) = getWindowBounds(totalPages, currentIndex) + + var displayLower by remember { mutableIntStateOf(targetLower) } + var displayUpper by remember { mutableIntStateOf(targetUpper) } + var prevTargetLower by remember { mutableIntStateOf(targetLower) } + + val slideOffset = remember { Animatable(0f) } + var isSliding by remember { mutableStateOf(false) } + var slideDirection by remember { mutableIntStateOf(0) } + val fadeProgress = remember { Animatable(0f) } + var fadeJob by remember { mutableStateOf(null) } + + LaunchedEffect(targetLower) { + if (targetLower != prevTargetLower && totalPages > MAX_VISIBLE_DOTS) { + fadeJob?.cancel() + slideOffset.stop() + fadeProgress.stop() + + val dir = if (targetLower > prevTargetLower) 1 else -1 + val edgeDotSize = with(density) { (HINT_DOT_SIZE.width + SPACING).toPx() } + val halfEdge = edgeDotSize / 2 + + isSliding = true + slideDirection = dir + fadeProgress.snapTo(0f) + + if (dir > 0) { + displayLower = prevTargetLower + displayUpper = targetUpper + slideOffset.snapTo(halfEdge) + } else { + displayLower = targetLower + displayUpper = prevTargetLower + MAX_VISIBLE_DOTS + slideOffset.snapTo(-halfEdge) + } + + prevTargetLower = targetLower + + fadeJob = launch { + fadeProgress.animateTo(1f, tween(ANIMATION_DURATION)) + } + slideOffset.animateTo( + if (dir > 0) -halfEdge else halfEdge, + tween(ANIMATION_DURATION), + ) + + displayLower = targetLower + displayUpper = targetUpper + slideOffset.snapTo(0f) + isSliding = false + slideDirection = 0 + } + } + val visibleIndices = (displayLower until displayUpper).toList() + + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + Row( + modifier = Modifier.offset { + IntOffset(slideOffset.value.roundToInt(), 0) + }, + horizontalArrangement = Arrangement.spacedBy(SPACING), + verticalAlignment = Alignment.CenterVertically, + ) { + visibleIndices.forEach { index -> + val dotAlpha = when { + !isSliding -> 1f + slideDirection > 0 && index == displayLower -> 1f - fadeProgress.value + slideDirection > 0 && index == displayUpper - 1 -> fadeProgress.value + slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress.value + slideDirection < 0 && index == displayLower -> fadeProgress.value + else -> 1f + } + + key(index) { + Dot( + index = index, + currentIndex = currentIndex, + totalPages = totalPages, + activeColor = activeIndicatorColor, + inactiveColor = inactiveIndicatorColor, + modifier = Modifier.graphicsLayer { alpha = dotAlpha }, + ) + } + } + } + } +} + +private fun getWindowBounds(totalPages: Int, currentIndex: Int): Pair { + if (totalPages <= MAX_VISIBLE_DOTS) { + return 0 to totalPages + } + val lowerBound = when { + currentIndex <= 1 -> 0 + currentIndex >= totalPages - 2 -> totalPages - MAX_VISIBLE_DOTS + else -> currentIndex - 2 + } + val upperBound = min(lowerBound + MAX_VISIBLE_DOTS, totalPages) + return lowerBound to upperBound +} + +private fun getDotSize(index: Int, currentIndex: Int, totalPages: Int): DpSize { + if (index == currentIndex) { + return CURRENT_DOT_SIZE + } + if (totalPages <= MAX_VISIBLE_DOTS) { + return NORMAL_DOT_SIZE + } + val params = DotSizeParams.create(index, currentIndex, totalPages) + return params.calculateSize() +} + +private class DotSizeParams private constructor( + val posInWindow: Int, + val currentPosInWindow: Int, + val hiddenLeft: Int, + val hiddenRight: Int, + val distanceFromCurrent: Int, +) { + private val lastPos = MAX_VISIBLE_DOTS - 1 + private val isCentered = currentPosInWindow == 2 && hiddenLeft >= 1 && hiddenRight >= 1 + + fun calculateSize(): DpSize = when { + isCentered -> getCenteredSize() + hiddenRight >= 1 -> getRightEdgeSize() + hiddenLeft >= 1 -> getLeftEdgeSize() + else -> NORMAL_DOT_SIZE + } + + private fun getCenteredSize(): DpSize = when (posInWindow) { + 0, lastPos -> HINT_DOT_SIZE + else -> NORMAL_DOT_SIZE + } + + private fun getRightEdgeSize(): DpSize { + val isLastPos = posInWindow == lastPos + val isSecondToLast = posInWindow == lastPos - 1 + val hasExtraHidden = hiddenRight >= MIN_HIDDEN_FOR_SMALL_DOT + val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT + val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT + + return when { + isLastPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE + isLastPos && isModerateDistance -> HINT_DOT_SIZE + isSecondToLast && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE + else -> NORMAL_DOT_SIZE + } + } + + private fun getLeftEdgeSize(): DpSize { + val isFirstPos = posInWindow == 0 + val isSecondPos = posInWindow == 1 + val hasExtraHidden = hiddenLeft >= MIN_HIDDEN_FOR_SMALL_DOT + val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT + val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT + + return when { + isFirstPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE + isFirstPos && isModerateDistance -> HINT_DOT_SIZE + isSecondPos && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE + else -> NORMAL_DOT_SIZE + } + } + + companion object { + fun create(index: Int, currentIndex: Int, totalPages: Int): DotSizeParams { + val (windowStart, windowEnd) = getWindowBounds(totalPages, currentIndex) + val posInWindow = index - windowStart + val currentPosInWindow = currentIndex - windowStart + return DotSizeParams( + posInWindow = posInWindow, + currentPosInWindow = currentPosInWindow, + hiddenLeft = windowStart, + hiddenRight = totalPages - windowEnd, + distanceFromCurrent = abs(posInWindow - currentPosInWindow), + ) + } + } +} + +@Composable +private fun Dot( + index: Int, + currentIndex: Int, + totalPages: Int, + activeColor: Color, + inactiveColor: Color, + modifier: Modifier = Modifier, +) { + val isActive = index == currentIndex + val size = getDotSize(index, currentIndex, totalPages) + + val animSpec = tween(ANIMATION_DURATION) + val colorSpec = tween(ANIMATION_DURATION) + + val animatedWidth by animateDpAsState(size.width, animSpec, label = "w$index") + val animatedHeight by animateDpAsState(size.height, animSpec, label = "h$index") + val animatedColor by animateColorAsState( + targetValue = if (isActive) activeColor else inactiveColor, + animationSpec = colorSpec, + label = "c$index", + ) + + val shape = RoundedCornerShape(animatedHeight / 2) + + Box( + modifier = modifier + .width(animatedWidth) + .height(animatedHeight) + .background(animatedColor, shape), + ) +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicatorPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4).forEach { page -> + TangemPagerIndicator(rememberPagerState(page) { 5 }) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicator6ItemsPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4, 5).forEach { page -> + TangemPagerIndicator(rememberPagerState(page) { 6 }) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicator7ItemsPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4, 5, 6).forEach { page -> + TangemPagerIndicator(rememberPagerState(page) { 7 }) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicator10ItemsPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).forEach { page -> + TangemPagerIndicator(rememberPagerState(page) { 10 }) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicatorSmallCountsPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + TangemPagerIndicator(rememberPagerState(0) { 1 }) + TangemPagerIndicator(rememberPagerState(1) { 2 }) + TangemPagerIndicator(rememberPagerState(1) { 3 }) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt index cae9546835..89aaa014aa 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.format.bigdecimal +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CRYPTO_FEE_FORMAT_THRESHOLD import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE @@ -9,6 +10,7 @@ import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE import com.tangem.utils.extensions.isNotWhitespace import java.math.BigDecimal import java.math.RoundingMode +import java.text.DecimalFormat import java.text.NumberFormat import java.util.Currency import java.util.Locale @@ -34,6 +36,17 @@ class BigDecimalCryptoFormatFull( override fun invoke(value: BigDecimal): String = defaultAmount()(value) } +open class BigDecimalCryptoFormatStyled( + val symbol: String, + val decimals: Int, + val spanStyleReference: SpanStyleReference, + val locale: Locale = Locale.getDefault(), + val shouldIgnoreSymbolPosition: Boolean = false, +) : BigDecimalFormatStyled { + + override fun invoke(value: BigDecimal): TextReference = defaultAmount(spanStyleReference)(value) +} + // == Initializers == fun BigDecimalFormatScope.crypto( @@ -61,6 +74,35 @@ fun BigDecimalFormatScope.crypto( ) } +fun BigDecimalFormatScope.cryptoStyled( + symbol: String, + decimals: Int, + spanStyleReference: SpanStyleReference, + locale: Locale = Locale.getDefault(), +): BigDecimalCryptoFormatStyled { + return BigDecimalCryptoFormatStyled( + symbol = symbol, + decimals = decimals, + spanStyleReference = spanStyleReference, + locale = locale, + ) +} + +fun BigDecimalFormatScope.cryptoStyled( + cryptoCurrency: CryptoCurrency, + spanStyleReference: SpanStyleReference, + ignoreSymbolPosition: Boolean = false, + locale: Locale = Locale.getDefault(), +): BigDecimalCryptoFormatStyled { + return BigDecimalCryptoFormatStyled( + symbol = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, + spanStyleReference = spanStyleReference, + shouldIgnoreSymbolPosition = ignoreSymbolPosition, + locale = locale, + ) +} + // == Formatters == fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value -> @@ -88,6 +130,51 @@ fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value -> } } +fun BigDecimalCryptoFormatStyled.defaultAmount(spanStyleReference: SpanStyleReference) = + BigDecimalFormatStyled { value -> + if (shouldIgnoreSymbolPosition) { + val formatter = NumberFormat.getInstance(locale).apply { + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + val formattedAmount = formatter.format(value) + + val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator + val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length + + combinedReference( + stringReference(formattedAmount.take(separatorIndex)), + styledStringReference(formattedAmount.drop(separatorIndex), spanStyleReference), + stringReference(NON_BREAKING_SPACE + symbol), + ) + } else { + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = usdCurrency + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + val formattedAmount = formatter.format(value) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.getSymbol(locale), + cryptoCurrencySymbol = symbol, + ) + + val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator + val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length + + combinedReference( + stringReference(formattedAmount.take(separatorIndex)), + styledStringReference(formattedAmount.drop(separatorIndex), spanStyleReference), + ) + } + } + fun BigDecimalCryptoFormat.shorted() = BigDecimalFormat { value -> val formatter = if (value.isMoreThanThreshold()) { NumberFormat.getCurrencyInstance(locale).apply { diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt index 5c41281344..a1f6549b2b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt @@ -1,9 +1,11 @@ package com.tangem.core.ui.format.bigdecimal +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN import com.tangem.utils.StringsSigns.TILDE_SIGN import java.math.BigDecimal import java.math.RoundingMode +import java.text.DecimalFormat import java.text.NumberFormat import java.util.Locale @@ -15,8 +17,16 @@ open class BigDecimalFiatFormat( override fun invoke(value: BigDecimal): String = defaultAmount()(value) } -// == Initializers == +open class BigDecimalFiatFormatStyled( + val fiatCurrencyCode: String, + val fiatCurrencySymbol: String, + val spanStyleReference: SpanStyleReference, + val locale: Locale = Locale.getDefault(), +) : BigDecimalFormatStyled { + override fun invoke(value: BigDecimal): TextReference = defaultAmount(spanStyleReference)(value) +} +//region == Initializers == fun BigDecimalFormatScope.fiat( fiatCurrencyCode: String, fiatCurrencySymbol: String, @@ -29,7 +39,20 @@ fun BigDecimalFormatScope.fiat( ) } -// == Formatters == +fun BigDecimalFormatScope.fiat( + fiatCurrencyCode: String, + fiatCurrencySymbol: String, + spanStyleReference: SpanStyleReference, + locale: Locale = Locale.getDefault(), +): BigDecimalFiatFormatStyled { + return BigDecimalFiatFormatStyled( + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + spanStyleReference = spanStyleReference, + locale = locale, + ) +} +// endregion == Formatters == /** * Formats fiat amount with default precision. @@ -58,6 +81,38 @@ fun BigDecimalFiatFormat.defaultAmount(): BigDecimalFormat = BigDecimalFormat { } } +fun BigDecimalFiatFormatStyled.defaultAmount(spanStyleReference: SpanStyleReference) = BigDecimalFormatStyled { value -> + val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode) + + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = formatterCurrency + maximumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + roundingMode = RoundingMode.HALF_UP + } + + val formattingAmount = if (value.isLessThanThreshold()) { + FIAT_FORMAT_THRESHOLD + } else { + value + } + + val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator + val formattedAmount = formatter.format(formattingAmount) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) + + val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length + + val wholePart = formattedAmount.take(separatorIndex) + val fractionalPart = formattedAmount.drop(separatorIndex) + + combinedReference( + if (formattingAmount.isLessThanThreshold()) stringReference(CAN_BE_LOWER_SIGN) else TextReference.EMPTY, + stringReference(wholePart), + styledStringReference(fractionalPart, spanStyleReference), + ) +} + /** * Formats fiat amount with default precision and adds tilde sign */ diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt index c685db7f1d..f7a87efe6a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt @@ -1,17 +1,27 @@ package com.tangem.core.ui.format.bigdecimal +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference import java.math.BigDecimal interface BigDecimalFormatScope { - companion object { val Empty = object : BigDecimalFormatScope {} } + companion object { + val Empty = object : BigDecimalFormatScope {} + } } fun interface BigDecimalFormat : (BigDecimal) -> String, BigDecimalFormatScope +fun interface BigDecimalFormatStyled : (BigDecimal) -> TextReference, BigDecimalFormatScope + inline fun BigDecimal.format(block: BigDecimalFormatScope.() -> BigDecimalFormat): String { return BigDecimalFormatScope.Empty.block()(this) } +inline fun BigDecimal.formatStyled(block: BigDecimalFormatScope.() -> BigDecimalFormatStyled): TextReference { + return BigDecimalFormatScope.Empty.block()(this) +} + inline fun BigDecimal?.format( fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, block: BigDecimalFormatScope.() -> BigDecimalFormat, @@ -20,10 +30,26 @@ inline fun BigDecimal?.format( return BigDecimalFormatScope.Empty.block()(this) } +inline fun BigDecimal?.formatStyled( + fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, + block: BigDecimalFormatScope.() -> BigDecimalFormatStyled, +): TextReference { + if (this == null) return stringReference(fallbackString) + return BigDecimalFormatScope.Empty.block()(this) +} + fun BigDecimal?.format( format: BigDecimalFormat, fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, ): String { if (this == null) return fallbackString return format(this) +} + +fun BigDecimal?.format( + format: BigDecimalFormatStyled, + fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, +): TextReference { + if (this == null) return stringReference(fallbackString) + return format(this) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt index 04801d50d7..150d6586d3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt @@ -16,6 +16,7 @@ object TangemColorPalette { val Dark4 = Color(0xFF3B3B3B) val Dark5 = Color(0xFF303030) val Dark6 = Color(0xFF1E1E1E) + val Dark7 = Color(0xFF171717) // endregion Dark // region Dark Alpha diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt index efa104abf2..b838ad7601 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt @@ -122,8 +122,8 @@ private fun lightThemeColors2(): TangemColors2 { val surface = TangemColors2.Surface( level1 = TangemColorPalette.White, level2 = TangemColorPalette.Light1V2, - level3 = TangemColorPalette.Light1V2, - level4 = TangemColorPalette.White, + level3 = TangemColorPalette.White, + level4 = TangemColorPalette.Light1V2, ) val controls = TangemColors2.Controls( backgroundChecked = TangemColorPalette.Dark6, @@ -270,8 +270,8 @@ private fun darkThemeColors2(): TangemColors2 { borderPrimary = TangemColorPalette.Light4, ) val surface = TangemColors2.Surface( - level1 = TangemColorPalette.Dark6, - level2 = TangemColorPalette.Black, + level1 = TangemColorPalette.Black, + level2 = TangemColorPalette.Dark7, level3 = TangemColorPalette.Dark6, level4 = TangemColorPalette.Dark5, )