Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-01 12:07:39 +02:00
parent ca85fd3a6d
commit 956bc7cd75
6 changed files with 40 additions and 45 deletions

View file

@ -35,15 +35,17 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
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.components.state.DonutSegment
import kotlin.math.min
import com.tangem.features.foryou.impl.R
import com.tangem.features.foryou.impl.components.state.DonutSegment
import kotlin.math.atan2
import kotlin.math.hypot
import kotlin.math.min
/**
* Ring (donut) chart drawn behind a center [content] slot.
@ -122,7 +124,6 @@ internal fun DonutChart(
Box(
modifier = modifier
.background(TangemTheme.colors3.bg.secondary)
.then(clickModifier)
.drawBehind {
val arc = arcRect(strokePx)
@ -240,11 +241,11 @@ private fun segmentIndexAt(
val outer = min(size.width, size.height) / 2f
val inner = outer - strokePx
val tolerance = strokePx * 0.4f
val dist = kotlin.math.hypot(dx, dy)
val dist = hypot(dx, dy)
if (dist < inner - tolerance || dist > outer + tolerance) return null
// Degrees clockwise from 3 o'clock — same convention as Canvas.drawArc.
val angle = Math.toDegrees(kotlin.math.atan2(dy, dx).toDouble()).toFloat().mod(360f)
val angle = Math.toDegrees(atan2(dy, dx).toDouble()).toFloat().mod(360f)
val sweeps = segments.map { it.weight.coerceIn(0f, 1f) * 360f }
val starts = sweeps.runningFold(startAngle) { acc, sweep -> acc + sweep }
@ -363,7 +364,7 @@ private fun PreviewDonutChart() {
style = TangemTheme.typography3.heading.medium,
)
Text(
text = stringResourceSafe(R.string.market_chart_buble_total_value),
text = stringResourceSafe(R.string.market_chart_bubble_total_value),
color = TangemTheme.colors3.text.secondary,
style = TangemTheme.typography3.body.medium,
)
@ -389,7 +390,7 @@ private fun PreviewDonutChartEmpty() {
segments = emptyList(),
) {
Text(
text = stringResourceSafe(R.string.market_chart_buble_no_data),
text = stringResourceSafe(R.string.market_chart_bubble_no_data),
color = TangemTheme.colors3.text.primary,
style = TangemTheme.typography3.heading.medium,
)

View file

@ -186,7 +186,7 @@ private fun PreviewDonutSegmentTooltip() {
expandedStates = remember { MutableTransitionState(true) },
title = "Ethereum",
fiatValue = "$5,720.22",
percent = "57.5%",
percent = "57.5",
)
}
}

View file

@ -39,21 +39,12 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* Canvas-based take on the glow divider: the line background and every blurred color blob are painted by
* hand inside a single `Box(Modifier.drawBehind { })`.
* One blurred color blob painted inside [CanvasGradientDivider].
*
* How it differs from [GlowDotsDivider] (which layers child `Box`es with [androidx.compose.ui.draw.blur]):
* here there are no child composables at all. Inside [androidx.compose.ui.draw.drawBehind] we clip to the
* capsule path, fill the base color, then draw each [GlowDot] as a circle whose native [Paint] carries a
* [BlurMaskFilter] the canvas equivalent of a Gaussian layer blur. The clip means a blob drawn past the
* line's bounds only paints its color onto the visible capsule.
*
* @param modifier Modifier for positioning. Lays out to [lineWidth]×[lineHeight].
* @param lineWidth Width of the capsule.
* @param lineHeight Height of the capsule.
* @param lineColor Solid base fill of the line.
* @param dots Color blobs; [GlowDot.offset] is the blob center relative to the line's top-center,
* [GlowDot.size] its diameter, [GlowDot.blur] the mask-blur radius.
* @param color Blob color.
* @param offset Blob center, relative to the line's top-center.
* @param height Blob height (its vertical diameter); the width is fixed by [DotWidth].
* @param blur Mask-blur radius applied via [BlurMaskFilter].
*/
internal data class CanvasGlowDot(
val color: Color,
@ -62,6 +53,17 @@ internal data class CanvasGlowDot(
val blur: Dp = 8.dp,
)
/**
* Canvas-based glow divider: the line background and every blurred color blob are painted by hand inside a
* single `Box(Modifier.drawBehind { })`, with no child composables. Inside [Modifier.drawBehind] we clip
* to the capsule path, fill the base [LineColor], then draw each [CanvasGlowDot] as an oval whose native
* [Paint] carries a [BlurMaskFilter] the canvas equivalent of a Gaussian layer blur. The clip means a blob
* drawn past the line's bounds only paints its color onto the visible capsule.
*
* @param modifier Modifier for positioning; the capsule height comes from the parent, so the glow scales
* with the divider's length.
* @param lineWidth Width of the capsule.
*/
@Suppress("MagicNumber", "LongParameterList", "LongMethod")
@Composable
internal fun CanvasGradientDivider(modifier: Modifier = Modifier, lineWidth: Dp = 2.dp) {
@ -163,10 +165,6 @@ private data class GlowDotColors(
val orange: Color,
)
/**
* Placeholder snake-scatter of the four Figma "selection colors", sized proportionally to [lineHeight]
* so the glow scales with the divider's length. Tune to match Figma.
*/
@Suppress("MagicNumber")
private fun canvasDefaultDots(lineHeight: Dp, colors: GlowDotColors): List<CanvasGlowDot> {
val step = lineHeight / 5

View file

@ -38,7 +38,6 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
@ -53,19 +52,18 @@ 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.pluralStringResourceSafe
import com.tangem.core.ui.extensions.stringReference
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 kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlin.Int
import com.tangem.features.foryou.impl.R
@Composable
internal fun MarketChart(marketChartState: MarketChartState, modifier: Modifier = Modifier) {
@ -149,7 +147,7 @@ private fun ColumnScope.DonutChartBlock(donutChartState: DonutChartState, cardBo
),
)
Text(
text = stringResourceSafe(R.string.market_chart_buble_total_value),
text = stringResourceSafe(R.string.market_chart_bubble_total_value),
color = TangemTheme.colors3.text.secondary,
style = TangemTheme.typography3.caption.medium,
maxLines = 1,
@ -159,7 +157,7 @@ private fun ColumnScope.DonutChartBlock(donutChartState: DonutChartState, cardBo
)
} else {
Text(
text = stringResourceSafe(R.string.market_chart_buble_no_data),
text = stringResourceSafe(R.string.market_chart_bubble_no_data),
color = TangemTheme.colors3.text.secondary,
style = TangemTheme.typography3.body.medium,
maxLines = 1,
@ -239,7 +237,7 @@ private fun DonutSegmentTooltipBlock(
@Suppress("MagicNumber")
private fun formatSegmentPercent(weight: Float): String {
val percent = weight.coerceIn(0f, 1f) * 100
return if (percent % 1f == 0f) "${percent.toInt()}" else "%.2f%".format(percent)
return if (percent % 1f == 0f) "${percent.toInt()}" else "%.2f".format(percent)
}
private val DonutStrokeWidth = 28.dp
@ -288,7 +286,7 @@ private fun AiInsightContent(aiInsightState: AiInsightState) {
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
onClick = currentState.askAiInsightClick,
size = TangemButtonSize.X9,
text = stringReference("Ask for AI summary"),
text = resourceReference(R.string.market_chart_ask_for_ai_summary_button),
)
}
is AiInsightState.Displayed -> {
@ -368,7 +366,7 @@ private fun previewMarketChartState(scenario: MarketChartPreviewScenario): Marke
topHoldingPercent = 0.41f,
aiInsightState = AiInsightState.Displayed(
"Your portfolio leans on a single asset BTC is 42% of holdings. Stablecoins add 23% " +
"buffer. Consider trimmng concentration for a smoother ride",
"buffer. Consider trimming concentration for a smoother ride",
),
donutChartState = previewLoadedDonut(),
)
@ -403,13 +401,13 @@ private fun previewLoadedDonut(): DonutChartState.Loaded = DonutChartState.Loade
fiatValue = "$5,720.22",
),
DonutSegment(
weight = 0.07f,
weight = 0.077f,
color = TangemTheme.colors3.border.accent.violet,
title = "Solana",
fiatValue = "$728.30",
),
DonutSegment(
weight = 0.06f,
weight = 0.0666f,
color = TangemTheme.colors3.border.accent.red,
title = "Polkadot",
fiatValue = "$624.26",

View file

@ -1,8 +1,5 @@
package com.tangem.features.foryou.impl.components.state
import kotlin.Float
import kotlin.collections.List
internal sealed class MarketChartState(
open val donutChartState: DonutChartState,
open val aiInsightState: AiInsightState,