Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-21 15:25:55 +03:00
parent 25c5142135
commit a96fb4b2f4
5 changed files with 255 additions and 30 deletions

View file

@ -768,6 +768,7 @@
<string name="markets_add_to_my_portfolio_unavailable_for_wallet_description">This asset is not available for this wallet</string> <string name="markets_add_to_my_portfolio_unavailable_for_wallet_description">This asset is not available for this wallet</string>
<string name="markets_add_token">Add</string> <string name="markets_add_token">Add</string>
<string name="markets_apy_placeholder">APY %s</string> <string name="markets_apy_placeholder">APY %s</string>
<string name="markets_common_market_price">Market Price</string>
<string name="markets_common_my_portfolio">My portfolio</string> <string name="markets_common_my_portfolio">My portfolio</string>
<string name="markets_common_title">Market</string> <string name="markets_common_title">Market</string>
<string name="markets_earn_common_title">Staking &amp; Yield mode</string> <string name="markets_earn_common_title">Staking &amp; Yield mode</string>

View file

@ -1,25 +1,152 @@
package com.tangem.features.markets.token.block.impl.ui package com.tangem.features.markets.token.block.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.charts.MarketChartMini
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.ds.button.SecondaryTangemButton
import com.tangem.core.ui.ds.button.TangemButtonShape
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.ds.row.token.internal.TokenRowPriceChangeContent
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.R as CoreR
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.token.block.impl.model.formatter.toChartType
import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM
import kotlinx.collections.immutable.toImmutableList
import kotlin.random.Random
private val ChartWidth: Dp = 52.dp
private val ChartHeight: Dp = 32.dp
@Suppress("UnusedParameter")
@Composable @Composable
internal fun TokenMarketBlock(tokenMarketBlockUM: TokenMarketBlockUM, modifier: Modifier = Modifier) { internal fun TokenMarketBlock(tokenMarketBlockUM: TokenMarketBlockUM, modifier: Modifier = Modifier) {
Box( TangemRowContainer(
modifier = modifier.fillMaxWidth(), modifier = modifier
contentAlignment = Alignment.Center, .fillMaxWidth()
.clip(RoundedCornerShape(TangemTheme.dimens2.x5))
.background(TangemTheme.colors2.surface.level3),
) { ) {
Text( Text(
text = "Market Block Redesign", text = stringResourceSafe(id = R.string.markets_common_market_price),
style = TangemTheme.typography.body2, style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors.text.primary1, color = TangemTheme.colors2.text.neutral.primary,
modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
) {
Text(
text = tokenMarketBlockUM.currentPrice.orEmpty(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.primary,
)
TokenRowPriceChangeContent(
priceChangeState = PriceChangeState.Content(
type = tokenMarketBlockUM.priceChangeType,
valueInPercent = tokenMarketBlockUM.h24Percent.orEmpty(),
),
isFlickering = false,
)
}
Box(
modifier = Modifier.layoutId(TangemRowLayoutId.END_BOTTOM),
) {
val chartModifier = Modifier.requiredSize(width = ChartWidth, height = ChartHeight)
if (tokenMarketBlockUM.chartData != null) {
MarketChartMini(
rawData = tokenMarketBlockUM.chartData,
type = tokenMarketBlockUM.priceChangeType.toChartType(),
modifier = chartModifier,
)
} else {
RectangleShimmer(modifier = chartModifier)
}
}
SecondaryTangemButton(
onClick = tokenMarketBlockUM.onClick,
tangemIconUM = TangemIconUM.Icon(iconRes = CoreR.drawable.ic_arrow_expand_24),
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X10,
modifier = Modifier
.layoutId(TangemRowLayoutId.TAIL)
.padding(start = TangemTheme.dimens2.x10),
) )
} }
} }
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TokenMarketBlock_Preview(
@PreviewParameter(TokenMarketBlockPreviewProvider::class) params: TokenMarketBlockUM,
) {
TangemThemePreviewRedesign {
TokenMarketBlock(
tokenMarketBlockUM = params,
modifier = Modifier.background(TangemTheme.colors2.surface.level3),
)
}
}
private class TokenMarketBlockPreviewProvider : PreviewParameterProvider<TokenMarketBlockUM> {
private val data = MarketChartRawData(
x = List(size = 20) { Random.nextFloat().toDouble() }.toImmutableList(),
y = List(size = 20) { Random.nextFloat().toDouble() }.toImmutableList(),
)
private val state = TokenMarketBlockUM(
currencySymbol = "XRP",
currentPrice = "0,5$",
currentPriceValue = null,
priceAnnotated = null,
h24Percent = "0,5%",
priceChangeType = PriceChangeType.UP,
chartData = data,
onClick = {},
)
override val values: Sequence<TokenMarketBlockUM>
get() = sequenceOf(
state,
state.copy(currentPrice = "0,0000000000012356786789$"),
state.copy(currentPrice = null, chartData = null),
state.copy(chartData = null),
)
}
// endregion

View file

@ -97,6 +97,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
TokenDetailsScreen( TokenDetailsScreen(
tokenDetailsUM = tokenDetailsUM, tokenDetailsUM = tokenDetailsUM,
tokenMarketBlockComponent = tokenMarketBlockComponent,
modifier = modifier, modifier = modifier,
) )
} else { } else {

View file

@ -3,9 +3,12 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
@ -15,14 +18,22 @@ import com.tangem.common.ui.notifications.notifications
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountIconUM import com.tangem.common.ui.account.AccountIconUM
import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
@ -44,17 +55,22 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlockHeight import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlockHeight
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import dev.chrisbanes.haze.HazeProgressive import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.HazeTint
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@Composable private val TopBarHeight: Dp = 64.dp
internal fun TokenDetailsScreen(tokenDetailsUM: TokenDetailsUM, modifier: Modifier = Modifier) { private val MarketBlockHorizontalPadding: Dp = 14.dp
val topAppBarUM = tokenDetailsUM.topAppBarUM
@Composable
internal fun TokenDetailsScreen(
tokenDetailsUM: TokenDetailsUM,
tokenMarketBlockComponent: TokenMarketBlockComponent?,
modifier: Modifier = Modifier,
) {
val statusBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() } val statusBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() }
val topBarHeight = 64.dp val partialCollapsedHeight = TopBarHeight + statusBarHeight
val partialCollapsedHeight = topBarHeight + statusBarHeight
val expandedHeight = TokenDetailsBalanceBlockHeight + partialCollapsedHeight val expandedHeight = TokenDetailsBalanceBlockHeight + partialCollapsedHeight
val behavior = rememberTangemExitUntilCollapsedScrollBehavior( val behavior = rememberTangemExitUntilCollapsedScrollBehavior(
@ -63,6 +79,7 @@ internal fun TokenDetailsScreen(tokenDetailsUM: TokenDetailsUM, modifier: Modifi
) )
val rootBackground by LocalRootBackgroundColor.current val rootBackground by LocalRootBackgroundColor.current
var marketBlockHeight by remember { mutableStateOf(0.dp) }
val notificationModifier = Modifier val notificationModifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = TangemTheme.dimens2.x4) .padding(horizontal = TangemTheme.dimens2.x4)
@ -80,16 +97,18 @@ internal fun TokenDetailsScreen(tokenDetailsUM: TokenDetailsUM, modifier: Modifi
collapsingPart = { collapsingPart = {
TokenDetailsBalanceBlock( TokenDetailsBalanceBlock(
balanceBlockUM = tokenDetailsUM.balanceBlockUM, balanceBlockUM = tokenDetailsUM.balanceBlockUM,
behavior = behavior,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.statusBarsPadding() .statusBarsPadding()
.padding(top = topBarHeight), .padding(top = TopBarHeight),
) )
}, },
body = { body = {
TokenDetailsBody( TokenDetailsBody(
tokenDetailsUM = tokenDetailsUM, tokenDetailsUM = tokenDetailsUM,
rootBackground = rootBackground, rootBackground = rootBackground,
bottomContentPadding = marketBlockHeight,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.nestedScroll(behavior.nestedScrollConnection), .nestedScroll(behavior.nestedScrollConnection),
@ -99,33 +118,90 @@ internal fun TokenDetailsScreen(tokenDetailsUM: TokenDetailsUM, modifier: Modifi
) )
} }
val hazeIntensity by animateFloatAsState( TokenDetailsTopBarOverlay(
targetValue = (behavior.state.collapsedFraction * 2f).coerceIn(0f, 1f), topAppBarUM = tokenDetailsUM.topAppBarUM,
label = "TopBarHazeIntensity", collapsedFraction = behavior.state.collapsedFraction,
rootBackground = rootBackground,
) )
Box(
modifier = Modifier.hazeEffectTangem { if (tokenMarketBlockComponent != null) {
fallbackTint = HazeTint(rootBackground.copy(alpha = hazeIntensity / 2f)) TokenDetailsMarketBlockOverlay(
progressive = HazeProgressive.verticalGradient( component = tokenMarketBlockComponent,
startIntensity = hazeIntensity, rootBackground = rootBackground,
endIntensity = 0f, onHeightChange = { marketBlockHeight = it },
preferPerformance = true, )
)
},
) {
TokenDetailsTopBar(topAppBarUM = topAppBarUM)
} }
} }
} }
@Composable
private fun TokenDetailsTopBarOverlay(
topAppBarUM: TokenDetailsTopAppBarUM,
collapsedFraction: Float,
rootBackground: Color,
) {
val hazeIntensity by animateFloatAsState(
targetValue = (collapsedFraction * 2f).coerceIn(0f, 1f),
label = "TopBarHazeIntensity",
)
Box(
modifier = Modifier.hazeEffectTangem {
fallbackTint = HazeTint(rootBackground.copy(alpha = hazeIntensity / 2f))
progressive = HazeProgressive.verticalGradient(
startIntensity = hazeIntensity,
endIntensity = 0f,
preferPerformance = true,
)
},
) {
TokenDetailsTopBar(topAppBarUM = topAppBarUM)
}
}
@Composable
private fun BoxScope.TokenDetailsMarketBlockOverlay(
component: TokenMarketBlockComponent,
rootBackground: Color,
onHeightChange: (Dp) -> Unit,
) {
val density = LocalDensity.current
BottomFade(
gradientBrush = Brush.verticalGradient(
colors = listOf(
rootBackground.copy(alpha = 0f),
rootBackground,
),
),
modifier = Modifier.align(Alignment.BottomCenter),
)
component.Content(
modifier = Modifier
.align(Alignment.BottomCenter)
.onSizeChanged { size ->
onHeightChange(with(density) { size.height.toDp() })
}
.navigationBarsPadding()
.padding(
horizontal = MarketBlockHorizontalPadding,
vertical = TangemTheme.dimens2.x1_5,
),
)
}
@Composable @Composable
private fun TokenDetailsBody( private fun TokenDetailsBody(
tokenDetailsUM: TokenDetailsUM, tokenDetailsUM: TokenDetailsUM,
rootBackground: Color, rootBackground: Color,
bottomContentPadding: Dp,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
itemModifier: Modifier = Modifier, itemModifier: Modifier = Modifier,
) { ) {
LazyColumn(modifier = modifier) { LazyColumn(
modifier = modifier,
contentPadding = PaddingValues(bottom = bottomContentPadding),
) {
notifications( notifications(
notifications = tokenDetailsUM.notifications, notifications = tokenDetailsUM.notifications,
contentColor = rootBackground, contentColor = rootBackground,
@ -150,6 +226,7 @@ private fun TokenDetailsBody(
private fun TokenDetailsScreen_Preview() { private fun TokenDetailsScreen_Preview() {
TangemThemePreviewRedesign { TangemThemePreviewRedesign {
TokenDetailsScreen( TokenDetailsScreen(
tokenMarketBlockComponent = null,
tokenDetailsUM = TokenDetailsUM( tokenDetailsUM = TokenDetailsUM(
topAppBarUM = TokenDetailsTopAppBarUM( topAppBarUM = TokenDetailsTopAppBarUM(
titleState = TitleState.WithAccount( titleState = TitleState.WithAccount(

View file

@ -16,6 +16,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -31,6 +33,9 @@ import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.ds.button.action.ActionButtons import com.tangem.core.ui.ds.button.action.ActionButtons
import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior
import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior
import com.tangem.core.ui.ds.topbar.collapsing.snapToExitUntilCollapsed
import com.tangem.core.ui.extensions.resolveAnnotatedReference import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
@ -45,13 +50,26 @@ import kotlinx.collections.immutable.persistentListOf
private val CurrencyIconSize: Dp = 70.dp private val CurrencyIconSize: Dp = 70.dp
private val NetworkBadgeSize: Dp = 24.dp private val NetworkBadgeSize: Dp = 24.dp
internal val TokenDetailsBalanceBlockHeight: Dp = 404.dp internal val TokenDetailsBalanceBlockHeight: Dp = 404.dp
private const val MIN_SCALE = 0.75f
private const val MAX_SCALE = 1f
@Composable @Composable
internal fun TokenDetailsBalanceBlock(balanceBlockUM: TokenDetailsBalanceBlockUM, modifier: Modifier = Modifier) { internal fun TokenDetailsBalanceBlock(
balanceBlockUM: TokenDetailsBalanceBlockUM,
behavior: TangemCollapsingAppBarBehavior,
modifier: Modifier = Modifier,
) {
val rootBackground by LocalRootBackgroundColor.current val rootBackground by LocalRootBackgroundColor.current
val collapsedFraction = behavior.state.collapsedFraction
val alpha = 1f - collapsedFraction
val scale = alpha.coerceIn(MIN_SCALE, MAX_SCALE)
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier modifier = modifier
.alpha(alpha)
.scale(scale)
.snapToExitUntilCollapsed(behavior)
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = TangemTheme.dimens2.x10), .padding(vertical = TangemTheme.dimens2.x10),
) { ) {
@ -165,6 +183,7 @@ private fun TokenDetailsBalanceBlock_Preview(
TangemThemePreviewRedesign { TangemThemePreviewRedesign {
TokenDetailsBalanceBlock( TokenDetailsBalanceBlock(
balanceBlockUM = params, balanceBlockUM = params,
behavior = rememberTangemExitUntilCollapsedScrollBehavior(),
modifier = Modifier.background(TangemTheme.colors2.surface.level2), modifier = Modifier.background(TangemTheme.colors2.surface.level2),
) )
} }