Updated on 2026-08-14
This commit is contained in:
parent
e49dc700ac
commit
9d273fb556
9 changed files with 673 additions and 12 deletions
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<Account, CurrencyIconState.CryptoPortfolio> {
|
||||
class AccountIconItemStateConverter(
|
||||
val size: AccountIconSize = AccountIconSize.Default,
|
||||
) : Converter<Account, CurrencyIconState.CryptoPortfolio> {
|
||||
|
||||
override fun convert(value: Account): CurrencyIconState.CryptoPortfolio = when (value) {
|
||||
is Account.CryptoPortfolio -> when {
|
||||
|
|
@ -13,11 +16,13 @@ object AccountIconItemStateConverter : Converter<Account, CurrencyIconState.Cryp
|
|||
char = value.accountName.toUM().value,
|
||||
color = value.icon.color.getUiColor(),
|
||||
isGrayscale = false,
|
||||
size = size,
|
||||
)
|
||||
else -> CurrencyIconState.CryptoPortfolio.Icon(
|
||||
resId = value.icon.value.getResId(),
|
||||
color = value.icon.color.getUiColor(),
|
||||
isGrayscale = false,
|
||||
size = size,
|
||||
)
|
||||
}
|
||||
is Account.Payment -> TODO("[REDACTED_JIRA]")
|
||||
|
|
|
|||
|
|
@ -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<NotificationUM>,
|
||||
|
|
@ -110,4 +128,103 @@ fun LazyListScope.notifications(
|
|||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<TangemMessageUM>?,
|
||||
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<TangemMessageUM>,
|
||||
) {
|
||||
TangemThemePreviewRedesign {
|
||||
val contentColor = TangemTheme.colors2.surface.level1
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.background(contentColor)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
stackedNotifications(
|
||||
notifications = params,
|
||||
containerColor = contentColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class StackedNotificationsPreviewProvider : PreviewParameterProvider<ImmutableList<TangemMessageUM>> {
|
||||
override val values: Sequence<ImmutableList<TangemMessageUM>>
|
||||
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
|
||||
|
|
@ -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<Job?>(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<Int, Int> {
|
||||
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<Dp>(ANIMATION_DURATION)
|
||||
val colorSpec = tween<Color>(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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue