Updated on 2026-08-14
This commit is contained in:
parent
88783a797e
commit
54d1ea4973
6 changed files with 96 additions and 93 deletions
|
|
@ -42,7 +42,7 @@ 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.DonutSegment
|
||||
import com.tangem.features.foryou.impl.components.state.DonutSegmentUM
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.hypot
|
||||
import kotlin.math.min
|
||||
|
|
@ -71,7 +71,7 @@ import kotlin.math.min
|
|||
* yet). Taps are hit-tested against the ring band only and reported via [onSegmentClick]; the chart is
|
||||
* interactive only when [onSegmentClick] is set **and** [segments] is non-empty.
|
||||
*
|
||||
* @param segments Slices, in priority order (index 0 is painted on top). See [DonutSegment.weight].
|
||||
* @param segments Slices, in priority order (index 0 is painted on top). See [DonutSegmentUM.weight].
|
||||
* @param modifier Modifier; should carry the overall size (e.g. `Modifier.size(240.dp)`).
|
||||
* @param selectedIndex Index of the currently selected slice, or `null` for no selection (nothing dimmed).
|
||||
* @param onSegmentClick Invoked on every tap inside the chart: with the tapped slice index, or with `null`
|
||||
|
|
@ -86,7 +86,7 @@ import kotlin.math.min
|
|||
@Suppress("MagicNumber", "LongParameterList", "LongMethod", "NamedArguments")
|
||||
@Composable
|
||||
internal fun DonutChart(
|
||||
segments: List<DonutSegment>,
|
||||
segments: List<DonutSegmentUM>,
|
||||
modifier: Modifier = Modifier,
|
||||
selectedIndex: Int? = null,
|
||||
onSegmentClick: ((index: Int?) -> Unit)? = null,
|
||||
|
|
@ -230,7 +230,7 @@ private fun segmentIndexAt(
|
|||
tap: Offset,
|
||||
size: Size,
|
||||
strokePx: Float,
|
||||
segments: List<DonutSegment>,
|
||||
segments: List<DonutSegmentUM>,
|
||||
startAngle: Float,
|
||||
): Int? {
|
||||
val cx = size.width / 2f
|
||||
|
|
@ -351,10 +351,10 @@ private fun PreviewDonutChart() {
|
|||
selectedIndex = selectedIndex,
|
||||
onSegmentClick = { index -> selectedIndex = index.takeIf { it != selectedIndex } },
|
||||
segments = listOf(
|
||||
DonutSegment(weight = 0.55f, color = TangemTheme.colors3.border.brand),
|
||||
DonutSegment(weight = 0.07f, color = TangemTheme.colors3.border.accent.violet),
|
||||
DonutSegment(weight = 0.06f, color = TangemTheme.colors3.border.accent.red),
|
||||
DonutSegment(weight = 0.05f, color = TangemTheme.colors3.border.accent.green),
|
||||
DonutSegmentUM(weight = 0.55f, color = TangemTheme.colors3.border.brand),
|
||||
DonutSegmentUM(weight = 0.07f, color = TangemTheme.colors3.border.accent.violet),
|
||||
DonutSegmentUM(weight = 0.06f, color = TangemTheme.colors3.border.accent.red),
|
||||
DonutSegmentUM(weight = 0.05f, color = TangemTheme.colors3.border.accent.green),
|
||||
),
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
|
|
|
|||
|
|
@ -54,51 +54,49 @@ import com.tangem.core.ui.ds2.surface.TangemSurface
|
|||
import com.tangem.core.ui.extensions.pluralStringResourceSafe
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalHazeState
|
||||
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.AiInsightState
|
||||
import com.tangem.features.foryou.impl.components.state.DonutChartState
|
||||
import com.tangem.features.foryou.impl.components.state.DonutSegment
|
||||
import com.tangem.features.foryou.impl.components.state.MarketChartState
|
||||
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 kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
internal fun MarketChart(marketChartState: MarketChartState, modifier: Modifier = Modifier) {
|
||||
val hazeState = LocalHazeState.current
|
||||
internal fun MarketChart(modifier: Modifier = Modifier, marketChart: MarketChartUM) {
|
||||
var cardBoundsInWindow by remember { mutableStateOf(Rect.Zero) }
|
||||
|
||||
TangemSurface(
|
||||
modifier = modifier
|
||||
.hazeSourceTangem(hazeState)
|
||||
.hazeSourceTangem()
|
||||
.onGloballyPositioned { cardBoundsInWindow = it.boundsInWindow() },
|
||||
color = TangemTheme.colors3.bg.secondary,
|
||||
) {
|
||||
Column {
|
||||
DonutChartBlock(marketChartState.donutChartState, cardBoundsInWindow)
|
||||
DonutChartBlock(marketChart.donutChart, cardBoundsInWindow)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
if (marketChartState is MarketChartState.Loaded) {
|
||||
if (marketChart is MarketChartUM.Loaded) {
|
||||
TopHoldingBlock(
|
||||
assetCount = marketChartState.assetCount,
|
||||
topHoldingPercent = marketChartState.topHoldingPercent,
|
||||
assetCount = marketChart.assetCount,
|
||||
topHoldingPercent = marketChart.topHoldingPercent,
|
||||
)
|
||||
} else {
|
||||
CantLoadDataBlock()
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
AiInsightContent(marketChartState.aiInsightState)
|
||||
AiInsightContent(marketChart.aiInsight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun ColumnScope.DonutChartBlock(donutChartState: DonutChartState, cardBoundsInWindow: Rect) {
|
||||
private fun ColumnScope.DonutChartBlock(donutChartUM: DonutChartUM, cardBoundsInWindow: Rect) {
|
||||
var selectedIndex by remember { mutableStateOf<Int?>(null) }
|
||||
val segments = donutChartState.donutSegmentList
|
||||
val segments = donutChartUM.donutSegmentList
|
||||
val scope = rememberCoroutineScope()
|
||||
var dismissJob by remember { mutableStateOf<Job?>(null) }
|
||||
var chartSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
|
|
@ -135,9 +133,9 @@ private fun ColumnScope.DonutChartBlock(donutChartState: DonutChartState, cardBo
|
|||
},
|
||||
segments = segments,
|
||||
) {
|
||||
if (donutChartState is DonutChartState.Loaded) {
|
||||
if (donutChartUM is DonutChartUM.Loaded) {
|
||||
Text(
|
||||
text = donutChartState.totalAmount,
|
||||
text = donutChartUM.totalAmount,
|
||||
color = TangemTheme.colors3.text.primary,
|
||||
style = TangemTheme.typography3.body.medium,
|
||||
maxLines = 1,
|
||||
|
|
@ -189,7 +187,7 @@ private fun ColumnScope.DonutChartBlock(donutChartState: DonutChartState, cardBo
|
|||
@Composable
|
||||
private fun DonutSegmentTooltipBlock(
|
||||
selectedIndex: Int?,
|
||||
segments: List<DonutSegment>,
|
||||
segments: List<DonutSegmentUM>,
|
||||
chartSize: IntSize,
|
||||
chartWindowOffset: Offset,
|
||||
cardBoundsInWindow: Rect,
|
||||
|
|
@ -273,13 +271,13 @@ private fun ColumnScope.CantLoadDataBlock() {
|
|||
// IntrinsicSize.Min lets the gradient divider match the AI text height — heightIn wouldn't achieve that.
|
||||
@Suppress("ModifierHeightWithText")
|
||||
@Composable
|
||||
private fun AiInsightContent(aiInsightState: AiInsightState) {
|
||||
private fun AiInsightContent(aiInsightUM: AiInsightUM) {
|
||||
AnimatedContent(
|
||||
targetState = aiInsightState,
|
||||
targetState = aiInsightUM,
|
||||
transitionSpec = { fadeIn().togetherWith(fadeOut()) },
|
||||
) { currentState ->
|
||||
when (currentState) {
|
||||
is AiInsightState.AskAiInsight -> {
|
||||
is AiInsightUM.AskAiInsight -> {
|
||||
SecondaryTangemButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -289,7 +287,7 @@ private fun AiInsightContent(aiInsightState: AiInsightState) {
|
|||
text = resourceReference(R.string.market_chart_ask_for_ai_summary_button),
|
||||
)
|
||||
}
|
||||
is AiInsightState.Displayed -> {
|
||||
is AiInsightUM.Displayed -> {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
|
|
@ -324,7 +322,7 @@ private fun AiInsightContent(aiInsightState: AiInsightState) {
|
|||
)
|
||||
}
|
||||
}
|
||||
AiInsightState.Hide -> {}
|
||||
AiInsightUM.Hide -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -350,7 +348,7 @@ private fun MarketChart_Preview(
|
|||
.background(TangemTheme.colors3.bg.primary)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
MarketChart(marketChartState = previewMarketChartState(scenario))
|
||||
MarketChart(marketChart = previewMarketChartState(scenario))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -361,58 +359,58 @@ private fun MarketChart_Preview(
|
|||
*/
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun previewMarketChartState(scenario: MarketChartPreviewScenario): MarketChartState = when (scenario) {
|
||||
MarketChartPreviewScenario.DISPLAYED -> MarketChartState.Loaded(
|
||||
private fun previewMarketChartState(scenario: MarketChartPreviewScenario): MarketChartUM = when (scenario) {
|
||||
MarketChartPreviewScenario.DISPLAYED -> MarketChartUM.Loaded(
|
||||
topHoldingPercent = 0.41f,
|
||||
aiInsightState = AiInsightState.Displayed(
|
||||
aiInsight = AiInsightUM.Displayed(
|
||||
"Your portfolio leans on a single asset – BTC is 42% of holdings. Stablecoins add 23% " +
|
||||
"buffer. Consider trimming concentration for a smoother ride",
|
||||
),
|
||||
donutChartState = previewLoadedDonut(),
|
||||
donutChart = previewLoadedDonut(),
|
||||
)
|
||||
MarketChartPreviewScenario.ASK_AI -> MarketChartState.Loaded(
|
||||
MarketChartPreviewScenario.ASK_AI -> MarketChartUM.Loaded(
|
||||
topHoldingPercent = 0.41f,
|
||||
aiInsightState = AiInsightState.AskAiInsight(askAiInsightClick = {}),
|
||||
donutChartState = previewLoadedDonut(),
|
||||
aiInsight = AiInsightUM.AskAiInsight(askAiInsightClick = {}),
|
||||
donutChart = previewLoadedDonut(),
|
||||
)
|
||||
MarketChartPreviewScenario.NO_AI -> MarketChartState.Loaded(
|
||||
MarketChartPreviewScenario.NO_AI -> MarketChartUM.Loaded(
|
||||
topHoldingPercent = 0.41f,
|
||||
aiInsightState = AiInsightState.Hide,
|
||||
donutChartState = DonutChartState.Loaded(
|
||||
aiInsight = AiInsightUM.Hide,
|
||||
donutChart = DonutChartUM.Loaded(
|
||||
totalAmount = "$10,12345678912.1333",
|
||||
donutSegmentList = listOf(
|
||||
DonutSegment(weight = 0.55f, color = TangemTheme.colors3.border.brand),
|
||||
DonutSegment(weight = 0.45f, color = TangemTheme.colors3.border.accent.green),
|
||||
DonutSegmentUM(weight = 0.55f, color = TangemTheme.colors3.border.brand),
|
||||
DonutSegmentUM(weight = 0.45f, color = TangemTheme.colors3.border.accent.green),
|
||||
),
|
||||
),
|
||||
)
|
||||
MarketChartPreviewScenario.NO_DATA -> MarketChartState.NoData
|
||||
MarketChartPreviewScenario.NO_DATA -> MarketChartUM.NoData
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun previewLoadedDonut(): DonutChartState.Loaded = DonutChartState.Loaded(
|
||||
private fun previewLoadedDonut(): DonutChartUM.Loaded = DonutChartUM.Loaded(
|
||||
totalAmount = "$10,123456.1333",
|
||||
donutSegmentList = listOf(
|
||||
DonutSegment(
|
||||
DonutSegmentUM(
|
||||
weight = 0.55f,
|
||||
color = TangemTheme.colors3.border.brand,
|
||||
title = "Ethereum",
|
||||
fiatValue = "$5,720.22",
|
||||
),
|
||||
DonutSegment(
|
||||
DonutSegmentUM(
|
||||
weight = 0.077f,
|
||||
color = TangemTheme.colors3.border.accent.violet,
|
||||
title = "Solana",
|
||||
fiatValue = "$728.30",
|
||||
),
|
||||
DonutSegment(
|
||||
DonutSegmentUM(
|
||||
weight = 0.0666f,
|
||||
color = TangemTheme.colors3.border.accent.red,
|
||||
title = "Polkadot",
|
||||
fiatValue = "$624.26",
|
||||
),
|
||||
DonutSegment(
|
||||
DonutSegmentUM(
|
||||
weight = 0.05f,
|
||||
color = TangemTheme.colors3.border.accent.green,
|
||||
title = "Tether",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import androidx.compose.ui.unit.IntRect
|
|||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.window.PopupPositionProvider
|
||||
import com.tangem.features.foryou.impl.components.state.DonutSegment
|
||||
import com.tangem.features.foryou.impl.components.state.DonutSegmentUM
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
|
@ -23,7 +23,7 @@ import kotlin.math.sin
|
|||
@Suppress("MagicNumber", "LongParameterList", "ComplexCondition")
|
||||
internal fun segmentTooltipPositionProvider(
|
||||
selectedIndex: Int?,
|
||||
segments: List<DonutSegment>,
|
||||
segments: List<DonutSegmentUM>,
|
||||
chartSize: IntSize,
|
||||
chartWindowOffset: Offset,
|
||||
strokePx: Float,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import androidx.compose.ui.graphics.Color
|
|||
* @param fiatValue Pre-formatted fiat value of the slice (e.g. `"$5,720.22"`). Shown in the selection
|
||||
* tooltip next to the share. Empty by default.
|
||||
*/
|
||||
internal data class DonutSegment(
|
||||
internal data class DonutSegmentUM(
|
||||
val weight: Float,
|
||||
val color: Color,
|
||||
val title: String = "",
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
package com.tangem.features.foryou.impl.components.state
|
||||
|
||||
internal sealed class MarketChartState(
|
||||
open val donutChartState: DonutChartState,
|
||||
open val aiInsightState: AiInsightState,
|
||||
) {
|
||||
data class Loaded(
|
||||
override val donutChartState: DonutChartState.Loaded,
|
||||
override val aiInsightState: AiInsightState = AiInsightState.Hide,
|
||||
/* from 0 to 1 */
|
||||
val topHoldingPercent: Float,
|
||||
) : MarketChartState(
|
||||
donutChartState = donutChartState,
|
||||
aiInsightState = aiInsightState,
|
||||
) {
|
||||
val assetCount: Int = donutChartState.donutSegmentList.size
|
||||
}
|
||||
|
||||
data object NoData : MarketChartState(
|
||||
donutChartState = DonutChartState.NoData,
|
||||
aiInsightState = AiInsightState.Hide,
|
||||
)
|
||||
}
|
||||
|
||||
internal sealed class DonutChartState(
|
||||
open val donutSegmentList: List<DonutSegment>,
|
||||
) {
|
||||
data class Loaded(
|
||||
val totalAmount: String,
|
||||
override val donutSegmentList: List<DonutSegment>,
|
||||
) : DonutChartState(donutSegmentList = donutSegmentList)
|
||||
|
||||
data object NoData : DonutChartState(donutSegmentList = emptyList())
|
||||
}
|
||||
|
||||
internal sealed class AiInsightState {
|
||||
data object Hide : AiInsightState()
|
||||
data class AskAiInsight(val askAiInsightClick: () -> Unit) : AiInsightState()
|
||||
data class Displayed(val text: String) : AiInsightState()
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.features.foryou.impl.components.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
internal sealed class MarketChartUM(
|
||||
open val donutChart: DonutChartUM,
|
||||
open val aiInsight: AiInsightUM,
|
||||
) {
|
||||
data class Loaded(
|
||||
override val donutChart: DonutChartUM.Loaded,
|
||||
override val aiInsight: AiInsightUM = AiInsightUM.Hide,
|
||||
/* from 0 to 1 */
|
||||
val topHoldingPercent: Float,
|
||||
) : MarketChartUM(
|
||||
donutChart = donutChart,
|
||||
aiInsight = aiInsight,
|
||||
) {
|
||||
val assetCount: Int = donutChart.donutSegmentList.size
|
||||
}
|
||||
|
||||
data object NoData : MarketChartUM(
|
||||
donutChart = DonutChartUM.NoData,
|
||||
aiInsight = AiInsightUM.Hide,
|
||||
)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
internal sealed class DonutChartUM(
|
||||
open val donutSegmentList: List<DonutSegmentUM>,
|
||||
) {
|
||||
data class Loaded(
|
||||
val totalAmount: String,
|
||||
override val donutSegmentList: List<DonutSegmentUM>,
|
||||
) : DonutChartUM(donutSegmentList = donutSegmentList)
|
||||
|
||||
data object NoData : DonutChartUM(donutSegmentList = emptyList())
|
||||
}
|
||||
|
||||
@Immutable
|
||||
internal sealed class AiInsightUM {
|
||||
data object Hide : AiInsightUM()
|
||||
data class AskAiInsight(val askAiInsightClick: () -> Unit) : AiInsightUM()
|
||||
data class Displayed(val text: String) : AiInsightUM()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue