Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-12 08:51:56 +01:00
parent d84315a40c
commit 871c8348a3
6 changed files with 351 additions and 23 deletions

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M8.502,3.196C9.155,2.045 10.845,2.045 11.499,3.196L11.561,3.315L12.925,6.193L16.147,6.577C17.578,6.747 18.215,8.524 17.108,9.533L14.744,11.687L15.364,14.789C15.658,16.262 14.082,17.295 12.842,16.619L10,15.069L7.159,16.619C5.919,17.295 4.343,16.262 4.637,14.789L5.256,11.688L2.892,9.533C1.785,8.524 2.422,6.748 3.853,6.577L7.076,6.193L8.44,3.315L8.502,3.196Z"
android:fillColor="#0099FF"
android:fillType="evenOdd"/>
</vector>

View file

@ -37,6 +37,7 @@ import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.markets.PriceChangeInterval
@ -81,6 +82,7 @@ private fun Content(
modifier: Modifier = Modifier,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
) {
val isRedesignEnabled = LocalRedesignEnabled.current
val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }
val lazyListState = rememberLazyListState()
@ -133,6 +135,7 @@ private fun Content(
state = state.body,
portfolioBlock = portfolioBlock,
relatedNews = state.relatedNews,
isRedesignEnabled = isRedesignEnabled,
)
}
}

View file

@ -0,0 +1,87 @@
package com.tangem.features.feed.ui.market.detailed.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun InformationTextBlock(
text: TextReference,
onInfoClick: () -> Unit,
modifier: Modifier = Modifier,
textStyle: TextStyle = TangemTheme.typography2.captionSemibold12,
textColor: Color = TangemTheme.colors2.text.neutral.secondary,
informationTextBlockIconPosition: InformationTextBlockIconPosition = InformationTextBlockIconPosition.START,
) {
val interactionSource = remember { MutableInteractionSource() }
val infoIcon: @Composable () -> Unit = {
IconButton(
modifier = Modifier.requiredSize(TangemTheme.dimens2.x4),
interactionSource = interactionSource,
onClick = onInfoClick,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x4),
imageVector = ImageVector.vectorResource(id = R.drawable.ic_information_24),
tint = TangemTheme.colors2.markers.iconGray,
contentDescription = null,
)
}
}
val contentText: @Composable () -> Unit = {
Text(
text = text.resolveReference(),
style = textStyle,
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
Row(
modifier = modifier
.clickable(
interactionSource = interactionSource,
indication = null,
onClick = onInfoClick,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
) {
when (informationTextBlockIconPosition) {
InformationTextBlockIconPosition.START -> {
infoIcon()
contentText()
}
InformationTextBlockIconPosition.END -> {
contentText()
infoIcon()
}
}
}
}
internal enum class InformationTextBlockIconPosition {
START, END
}

View file

@ -1,11 +1,7 @@
package com.tangem.features.feed.ui.market.detailed.components
import androidx.annotation.FloatRange
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -22,6 +18,7 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.impl.R
import kotlin.math.round
@ -34,6 +31,28 @@ internal fun ScoreStarsBlock(
horizontalSpacing: Dp,
scoreTextStyle: TextStyle,
modifier: Modifier = Modifier,
) {
if (LocalRedesignEnabled.current) {
ScoreStarsBlockV2(
score = score,
modifier = modifier,
)
} else {
ScoreStarsBlockV1(
score = score,
horizontalSpacing = horizontalSpacing,
scoreTextStyle = scoreTextStyle,
modifier = modifier,
)
}
}
@Composable
private fun ScoreStarsBlockV1(
score: Float,
horizontalSpacing: Dp,
scoreTextStyle: TextStyle,
modifier: Modifier = Modifier,
) {
val rounded = score.roundTo1decimal()
val percentage = rounded / STARS_COUNT
@ -51,16 +70,38 @@ internal fun ScoreStarsBlock(
}
}
@Composable
private fun ScoreStarsBlockV2(score: Float, modifier: Modifier = Modifier) {
val rounded = score.roundTo1decimal()
val percentage = rounded / STARS_COUNT
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
Stars(fraction = percentage)
}
}
@Suppress("MagicNumber")
@Composable
private fun Stars(@FloatRange(0.0, 1.0) fraction: Float = 0f) {
if (LocalRedesignEnabled.current) {
StarsV2(fraction)
} else {
StarsV1(fraction)
}
}
@Suppress("MagicNumber")
@Composable
private fun StarsV1(@FloatRange(0.0, 1.0) fraction: Float = 0f) {
val grayColor = TangemTheme.colors.icon.inactive
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
verticalAlignment = Alignment.CenterVertically,
) {
repeat(times = 5) { i ->
repeat(times = STARS_COUNT) { i ->
Box(
modifier = Modifier.size(TangemTheme.dimens.size16),
contentAlignment = Alignment.Center,
@ -94,6 +135,49 @@ private fun Stars(@FloatRange(0.0, 1.0) fraction: Float = 0f) {
}
}
@Suppress("MagicNumber")
@Composable
private fun StarsV2(@FloatRange(0.0, 1.0) fraction: Float = 0f) {
val grayColor = TangemTheme.colors2.markers.iconGray
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
verticalAlignment = Alignment.CenterVertically,
) {
repeat(times = STARS_COUNT) { i ->
Box(
modifier = Modifier.size(TangemTheme.dimens2.x5),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier
.requiredSize(16.dp)
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
.drawWithCache {
onDrawWithContent {
val starFraction = ((fraction - i * 0.2) / 0.2).coerceIn(0.0, 1.0)
val starFractionFloat = starFraction
.toFloat()
.roundTo1decimal()
drawContent()
drawRect(
color = grayColor,
topLeft = Offset(x = size.width * starFractionFloat, y = 0f),
size = Size(size.width * (1 - starFractionFloat), size.height),
blendMode = BlendMode.SrcIn,
)
}
},
imageVector = ImageVector.vectorResource(R.drawable.ic_star_filled_20),
contentDescription = null,
tint = TangemTheme.colors2.markers.iconBlue,
)
}
}
}
}
@Suppress("MagicNumber")
private fun Float.roundTo1decimal(): Float {
return round(this * 10) / 10

View file

@ -2,33 +2,44 @@ package com.tangem.features.feed.ui.market.detailed.components
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.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.text.TooltipText
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
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.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.utils.PreviewShimmerContainer
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.market.detailed.state.SecurityScoreUM
@Composable
internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
SecurityScoreBlockV2(state, modifier)
} else {
SecurityScoreBlockV1(state, modifier)
}
}
@Composable
private fun SecurityScoreBlockV1(state: SecurityScoreUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
@ -66,8 +77,92 @@ internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Mod
}
}
@Composable
private fun SecurityScoreBlockV2(state: SecurityScoreUM, modifier: Modifier = Modifier) {
TangemRowContainer(modifier = modifier) {
Text(
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.START_TOP),
text = "${state.score}",
color = TangemTheme.colors2.text.neutral.primary,
style = TangemTheme.typography2.headingBold28,
)
InformationTextBlock(
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.START_BOTTOM),
text = resourceReference(R.string.markets_token_details_security_score),
onInfoClick = state.onInfoClick,
textColor = TangemTheme.colors2.text.neutral.primary,
informationTextBlockIconPosition = InformationTextBlockIconPosition.END,
)
Text(
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM),
text = state.description.resolveReference(),
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
ScoreStarsBlock(
modifier = Modifier
.padding(bottom = 16.dp)
.layoutId(layoutId = TangemRowLayoutId.END_TOP),
score = state.score,
scoreTextStyle = TangemTheme.typography.body1,
horizontalSpacing = TangemTheme.dimens.spacing8,
)
}
}
@Composable
internal fun SecurityScoreBlockPlaceholder(modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
SecurityScoreBlockPlaceholderV2(modifier)
} else {
SecurityScoreBlockPlaceholderV1(modifier)
}
}
@Composable
private fun SecurityScoreBlockPlaceholderV2(modifier: Modifier = Modifier) {
TangemRowContainer(modifier = modifier) {
TextShimmer(
modifier = Modifier
.width(74.dp)
.layoutId(layoutId = TangemRowLayoutId.START_TOP),
style = TangemTheme.typography2.headingBold28,
radius = TangemTheme.dimens2.x25,
)
TextShimmer(
modifier = Modifier
.width(96.dp)
.layoutId(layoutId = TangemRowLayoutId.START_BOTTOM),
style = TangemTheme.typography2.captionSemibold12,
radius = TangemTheme.dimens2.x25,
)
TextShimmer(
modifier = Modifier
.width(120.dp)
.layoutId(layoutId = TangemRowLayoutId.END_TOP),
style = TangemTheme.typography2.headingBold28,
radius = TangemTheme.dimens2.x25,
)
TextShimmer(
modifier = Modifier
.width(72.dp)
.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM),
style = TangemTheme.typography2.captionSemibold12,
radius = TangemTheme.dimens2.x25,
)
}
}
@Composable
private fun SecurityScoreBlockPlaceholderV1(modifier: Modifier = Modifier) {
Row(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
@ -109,7 +204,7 @@ internal fun SecurityScoreBlockPlaceholder(modifier: Modifier = Modifier) {
@Preview(widthDp = 328, showBackground = true, locale = "ru")
@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreview() {
private fun ContentPreviewV1() {
TangemThemePreview {
SecurityScoreBlock(
state = SecurityScoreUM(
@ -124,7 +219,7 @@ private fun ContentPreview() {
@Preview(widthDp = 328, showBackground = true)
@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewPlaceholder() {
private fun PreviewPlaceholderV1() {
TangemThemePreview {
PreviewShimmerContainer(
shimmerContent = {
@ -133,8 +228,36 @@ private fun PreviewPlaceholder() {
)
},
actualContent = {
ContentPreview()
ContentPreviewV1()
},
)
}
}
@Preview(widthDp = 328, showBackground = true)
@Preview(widthDp = 328, showBackground = true, locale = "ru")
@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreviewV2() {
CompositionLocalProvider(
LocalRedesignEnabled provides true,
) {
TangemThemePreviewRedesign {
Column(modifier = Modifier.background(TangemTheme.colors2.surface.level2)) {
SecurityScoreBlock(
state = SecurityScoreUM(
score = 3.5f,
description = stringReference("Based on 3 ratings"),
onInfoClick = {},
),
)
SpacerH(10.dp)
SecurityScoreBlockPlaceholder(
modifier = Modifier.fillMaxWidth(),
)
}
}
}
}

View file

@ -22,6 +22,7 @@ import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM.R
@Suppress("CanBeNonNullable")
internal fun LazyListScope.tokenMarketDetailsBody(
isRedesignEnabled: Boolean,
state: MarketsTokenDetailsUM.Body,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
relatedNews: RelatedNews,
@ -40,7 +41,7 @@ internal fun LazyListScope.tokenMarketDetailsBody(
aboutCoinHeader()
loadingInfoBlocks()
loadingInfoBlocks(isRedesignEnabled)
}
is MarketsTokenDetailsUM.Body.Content -> {
if (state.description != null) {
@ -59,7 +60,10 @@ internal fun LazyListScope.tokenMarketDetailsBody(
aboutCoinHeader()
infoBlocksList(state.infoBlocks)
infoBlocksList(
state = state.infoBlocks,
isRedesignEnabled = isRedesignEnabled,
)
}
is MarketsTokenDetailsUM.Body.Error -> {
error(state)
@ -112,7 +116,7 @@ private fun LazyListScope.description(description: MarketsTokenDetailsUM.Descrip
}
}
internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.InformationBlocks) {
internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.InformationBlocks, isRedesignEnabled: Boolean) {
if (state.insights != null) {
item("insights") {
InsightsBlock(
@ -122,7 +126,7 @@ internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.Informati
}
}
if (state.securityScore != null) {
if (state.securityScore != null && !isRedesignEnabled) {
item("securityScore") {
SecurityScoreBlock(
modifier = Modifier.blockPaddings(),
@ -156,6 +160,15 @@ internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.Informati
)
}
if (state.securityScore != null && isRedesignEnabled) {
item("securityScore") {
SecurityScoreBlock(
modifier = Modifier.blockPaddings(),
state = state.securityScore,
)
}
}
if (state.links != null) {
item("links") {
LinksBlock(
@ -166,15 +179,17 @@ internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.Informati
}
}
private fun LazyListScope.loadingInfoBlocks() {
private fun LazyListScope.loadingInfoBlocks(isRedesignEnabled: Boolean) {
item("insights-loading") {
InsightsBlockPlaceholder(
modifier = Modifier.blockPaddings(),
)
}
item("securityScore-loading") {
SecurityScoreBlockPlaceholder(modifier = Modifier.blockPaddings())
if (!isRedesignEnabled) {
item("securityScore-loading") {
SecurityScoreBlockPlaceholder(modifier = Modifier.blockPaddings())
}
}
item("metrics-loading") {
@ -189,6 +204,12 @@ private fun LazyListScope.loadingInfoBlocks() {
ListedOnBlockPlaceholder(modifier = Modifier.blockPaddings())
}
if (isRedesignEnabled) {
item("securityScore-loading") {
SecurityScoreBlockPlaceholder(modifier = Modifier.blockPaddings())
}
}
item("links-loading") {
LinksBlockPlaceholder(modifier = Modifier.blockPaddings())
}