Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-03 15:33:29 +05:00
parent 31a8ee5dd4
commit 7c3b952ee8
7 changed files with 170 additions and 76 deletions

View file

@ -8,18 +8,9 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
@ -43,7 +34,10 @@ import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.foryou.impl.R
import com.tangem.features.foryou.impl.components.state.DonutSegmentColor
import com.tangem.features.foryou.impl.components.state.DonutSegmentUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
import kotlin.math.atan2
import kotlin.math.hypot
@ -88,7 +82,7 @@ import kotlin.math.min
@Suppress("MagicNumber", "LongParameterList", "LongMethod", "NamedArguments")
@Composable
internal fun DonutChart(
segments: List<DonutSegmentUM>,
segments: ImmutableList<DonutSegmentUM>,
modifier: Modifier = Modifier,
selectedIndex: Int? = null,
onSegmentClick: ((index: Int?) -> Unit)? = null,
@ -100,6 +94,11 @@ internal fun DonutChart(
val strokePx = with(LocalDensity.current) { strokeWidth.toPx() }
val dimOverlayColor = TangemTheme.colors3.border.inverse.tertiary
// Resolve each slice's themed colour once, here in composition (the palette reads TangemTheme, which
// isn't available inside the drawBehind DrawScope). Order is preserved 1:1 with [segments] so slice i
// keeps the colour its producer assigned by rank — the draw pass below indexes by i, not by paint order.
val segmentColors = segments.map { it.color.getColor() }
// Fade the dim in/out in step with the segment tooltip's pop-in (same spring as DonutSegmentTooltip).
val dimProgress by animateFloatAsState(
targetValue = if (selectedIndex != null) 1f else 0f,
@ -175,7 +174,7 @@ internal fun DonutChart(
for (i in segments.indices.reversed()) {
if (sweeps[i] <= 0f) continue
drawArc(
color = segments[i].color,
color = segmentColors[i],
startAngle = starts[i],
sweepAngle = sweeps[i],
useCenter = false,
@ -352,28 +351,28 @@ private fun PreviewDonutChart() {
modifier = Modifier.size(260.dp),
selectedIndex = selectedIndex,
onSegmentClick = { index -> selectedIndex = index.takeIf { it != selectedIndex } },
segments = listOf(
segments = persistentListOf(
DonutSegmentUM(
weight = BigDecimal(0.55),
color = TangemTheme.colors3.border.brand,
color = DonutSegmentColor.Brand,
title = stringReference("Ethereum"),
fiatValue = stringReference("$5,720.22"),
),
DonutSegmentUM(
weight = BigDecimal(0.07),
color = TangemTheme.colors3.border.accent.violet,
color = DonutSegmentColor.Violet,
title = stringReference("Solana"),
fiatValue = stringReference("$728.30"),
),
DonutSegmentUM(
weight = BigDecimal(0.06),
color = TangemTheme.colors3.border.accent.red,
color = DonutSegmentColor.Red,
title = stringReference("Polkadot"),
fiatValue = stringReference("$624.26"),
),
DonutSegmentUM(
weight = BigDecimal(0.05),
color = TangemTheme.colors3.border.accent.green,
color = DonutSegmentColor.Green,
title = stringReference("Tether"),
fiatValue = stringReference("$520.18"),
),
@ -409,7 +408,7 @@ private fun PreviewDonutChartEmpty() {
) {
DonutChart(
modifier = Modifier.size(260.dp),
segments = emptyList(),
segments = persistentListOf(),
) {
Text(
text = stringResourceSafe(R.string.market_chart_bubble_no_data),

View file

@ -8,27 +8,10 @@ import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@ -51,21 +34,14 @@ import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.ds.button.SecondaryTangemButton
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.ds2.surface.TangemSurface
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.pluralStringResourceSafe
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.foryou.impl.R
import com.tangem.features.foryou.impl.components.state.AiInsightUM
import com.tangem.features.foryou.impl.components.state.DonutChartUM
import com.tangem.features.foryou.impl.components.state.DonutSegmentUM
import com.tangem.features.foryou.impl.components.state.MarketChartUM
import com.tangem.features.foryou.impl.components.state.*
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.math.BigDecimal
@ -80,7 +56,7 @@ internal fun MarketChart(marketChart: MarketChartUM, modifier: Modifier = Modifi
.onGloballyPositioned { cardBoundsInWindow = it.boundsInWindow() },
color = TangemTheme.colors3.bg.secondary,
) {
Column {
Column(modifier = Modifier.fillMaxWidth()) {
DonutChartBlock(marketChart.donutChart, cardBoundsInWindow)
Spacer(modifier = Modifier.height(16.dp))
if (marketChart is MarketChartUM.Loaded) {
@ -353,10 +329,7 @@ private fun MarketChart_Preview(
}
}
/**
* Maps a [scenario] to the state shown. Built inside a `@Composable` (not the [PreviewParameterProvider])
* because the segment colors come from [TangemTheme.colors3], which can only be read in composition.
*/
/** Maps a [scenario] to the state shown in the preview. */
@Suppress("MagicNumber")
@Composable
private fun previewMarketChartState(scenario: MarketChartPreviewScenario): MarketChartUM = when (scenario) {
@ -378,16 +351,16 @@ private fun previewMarketChartState(scenario: MarketChartPreviewScenario): Marke
aiInsight = AiInsightUM.Hide,
donutChart = DonutChartUM.Loaded(
totalAmount = "$10,12345678912.1333",
donutSegmentList = listOf(
donutSegmentList = persistentListOf(
DonutSegmentUM(
weight = BigDecimal(0.55),
color = TangemTheme.colors3.border.brand,
color = DonutSegmentColor.Brand,
title = stringReference("Ethereum"),
fiatValue = stringReference("$5,720.22"),
),
DonutSegmentUM(
weight = BigDecimal(0.45),
color = TangemTheme.colors3.border.accent.green,
color = DonutSegmentColor.Green,
title = stringReference("Solana"),
fiatValue = stringReference("$728.30"),
),
@ -401,28 +374,28 @@ private fun previewMarketChartState(scenario: MarketChartPreviewScenario): Marke
@Composable
private fun previewLoadedDonut(): DonutChartUM.Loaded = DonutChartUM.Loaded(
totalAmount = "$10,123456.1333",
donutSegmentList = listOf(
donutSegmentList = persistentListOf(
DonutSegmentUM(
weight = BigDecimal(0.55),
color = TangemTheme.colors3.border.brand,
color = DonutSegmentColor.Brand,
title = stringReference("Ethereum"),
fiatValue = stringReference("$5,720.22"),
),
DonutSegmentUM(
weight = BigDecimal(0.077),
color = TangemTheme.colors3.border.accent.violet,
color = DonutSegmentColor.Violet,
title = stringReference("Solana"),
fiatValue = stringReference("$728.30"),
),
DonutSegmentUM(
weight = BigDecimal(0.0666),
color = TangemTheme.colors3.border.accent.red,
color = DonutSegmentColor.Red,
title = stringReference("Polkadot"),
fiatValue = stringReference("$624.26"),
),
DonutSegmentUM(
weight = BigDecimal(0.05),
color = TangemTheme.colors3.border.accent.green,
color = DonutSegmentColor.Green,
title = stringReference("Tether"),
fiatValue = stringReference("$520.18"),
),

View file

@ -1,8 +1,11 @@
package com.tangem.features.foryou.impl.components.state
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import java.math.BigDecimal
/**
@ -12,7 +15,8 @@ import java.math.BigDecimal
* contiguously; whatever is left after `sum(weight)` shows through as the track.
* For a portfolio where slices sum to 1f the ring fills completely and no track is visible.
* Also doubles as the slice's share for the selection tooltip (rendered as `weight * 100%`).
* @param color Solid fill of the slice.
* @param color Palette entry for the slice's solid fill. Assigned by the producer in segment order, so the
* slice's colour follows its rank; [DonutChart] resolves it to a themed [Color] in composition.
* @param title Human-readable name of the asset this slice represents (e.g. `"Ethereum"`). Shown in the
* selection tooltip. Empty by default for slices that don't need a label.
* @param fiatValue Pre-formatted fiat value of the slice (e.g. `"$5,720.22"`). Shown in the selection
@ -21,8 +25,27 @@ import java.math.BigDecimal
@Immutable
internal data class DonutSegmentUM(
val color: Color,
val color: DonutSegmentColor,
val weight: BigDecimal,
val title: TextReference,
val fiatValue: TextReference,
)
)
internal enum class DonutSegmentColor {
Brand,
Violet,
Red,
Green,
;
@ReadOnlyComposable
@Composable
fun getColor(): Color {
return when (this) {
Brand -> TangemTheme.colors3.border.brand
Violet -> TangemTheme.colors3.border.accent.violet
Red -> TangemTheme.colors3.border.accent.red
Green -> TangemTheme.colors3.border.accent.green
}
}
}

View file

@ -2,6 +2,8 @@ package com.tangem.features.foryou.impl.components.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Immutable
internal sealed class MarketChartUM(
@ -27,14 +29,14 @@ internal sealed class MarketChartUM(
@Immutable
internal sealed class DonutChartUM(
open val donutSegmentList: List<DonutSegmentUM>,
open val donutSegmentList: ImmutableList<DonutSegmentUM>,
) {
data class Loaded(
val totalAmount: String,
override val donutSegmentList: List<DonutSegmentUM>,
override val donutSegmentList: ImmutableList<DonutSegmentUM>,
) : DonutChartUM(donutSegmentList = donutSegmentList)
data object NoData : DonutChartUM(donutSegmentList = emptyList())
data object NoData : DonutChartUM(donutSegmentList = persistentListOf())
}
@Immutable

View file

@ -0,0 +1,26 @@
package com.tangem.features.foryou.impl.model
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds2.messagebanner.TangemMessageBanner
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_cloud_exclamation_20
import com.tangem.features.foryou.impl.R
@Immutable
internal sealed class ForYouNotification(val state: TangemMessageBanner.State) {
data object UsedOutdatedData : ForYouNotification(
state = TangemMessageBanner.State(
title = resourceReference(R.string.warning_some_token_balances_not_updated),
iconEnd = TangemIconUM.Icon(
imageVector = Icons.ic_cloud_exclamation_20,
tintReference = { TangemTheme.colors3.icon.primary },
),
variant = TangemMessageBanner.Variant.Warning,
isShowGlowRing = false,
),
)
}