diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 2306a9be2e..da1a8d484c 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -768,6 +768,7 @@ This asset is not available for this wallet Add APY %s + Market Price My portfolio Market Staking & Yield mode diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt index 493d0af09c..66ce42ea67 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt @@ -1,25 +1,152 @@ 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.Row 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.runtime.Composable 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.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.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 kotlinx.collections.immutable.toImmutableList +import kotlin.random.Random + +private val ChartWidth: Dp = 52.dp +private val ChartHeight: Dp = 32.dp -@Suppress("UnusedParameter") @Composable internal fun TokenMarketBlock(tokenMarketBlockUM: TokenMarketBlockUM, modifier: Modifier = Modifier) { - Box( - modifier = modifier.fillMaxWidth(), - contentAlignment = Alignment.Center, + TangemRowContainer( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(TangemTheme.dimens2.x5)) + .background(TangemTheme.colors2.surface.level3), ) { Text( - text = "Market Block Redesign", - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, + text = stringResourceSafe(id = R.string.markets_common_market_price), + style = TangemTheme.typography2.bodySemibold16, + 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), ) } -} \ No newline at end of file +} + +// 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 { + + 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 + get() = sequenceOf( + state, + state.copy(currentPrice = "0,0000000000012356786789$"), + state.copy(currentPrice = null, chartData = null), + state.copy(chartData = null), + ) +} +// endregion \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt index 639595b1f4..035e4511bf 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt @@ -97,6 +97,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( TokenDetailsScreen( tokenDetailsUM = tokenDetailsUM, + tokenMarketBlockComponent = tokenMarketBlockComponent, modifier = modifier, ) } else { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 14147b34e3..0783260183 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -3,9 +3,12 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui import android.content.res.Configuration import androidx.compose.animation.core.animateFloatAsState 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding 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.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.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp 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.currency.icon.CurrencyIconState 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.ui.components.TokenDetailsBalanceBlock 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.HazeTint import kotlinx.collections.immutable.persistentListOf -@Composable -internal fun TokenDetailsScreen(tokenDetailsUM: TokenDetailsUM, modifier: Modifier = Modifier) { - val topAppBarUM = tokenDetailsUM.topAppBarUM +private val TopBarHeight: Dp = 64.dp +private val MarketBlockHorizontalPadding: Dp = 14.dp +@Composable +internal fun TokenDetailsScreen( + tokenDetailsUM: TokenDetailsUM, + tokenMarketBlockComponent: TokenMarketBlockComponent?, + modifier: Modifier = Modifier, +) { 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 behavior = rememberTangemExitUntilCollapsedScrollBehavior( @@ -63,6 +79,7 @@ internal fun TokenDetailsScreen(tokenDetailsUM: TokenDetailsUM, modifier: Modifi ) val rootBackground by LocalRootBackgroundColor.current + var marketBlockHeight by remember { mutableStateOf(0.dp) } val notificationModifier = Modifier .fillMaxWidth() .padding(horizontal = TangemTheme.dimens2.x4) @@ -80,16 +97,18 @@ internal fun TokenDetailsScreen(tokenDetailsUM: TokenDetailsUM, modifier: Modifi collapsingPart = { TokenDetailsBalanceBlock( balanceBlockUM = tokenDetailsUM.balanceBlockUM, + behavior = behavior, modifier = Modifier .fillMaxWidth() .statusBarsPadding() - .padding(top = topBarHeight), + .padding(top = TopBarHeight), ) }, body = { TokenDetailsBody( tokenDetailsUM = tokenDetailsUM, rootBackground = rootBackground, + bottomContentPadding = marketBlockHeight, modifier = Modifier .fillMaxSize() .nestedScroll(behavior.nestedScrollConnection), @@ -99,33 +118,90 @@ internal fun TokenDetailsScreen(tokenDetailsUM: TokenDetailsUM, modifier: Modifi ) } - val hazeIntensity by animateFloatAsState( - targetValue = (behavior.state.collapsedFraction * 2f).coerceIn(0f, 1f), - label = "TopBarHazeIntensity", + TokenDetailsTopBarOverlay( + topAppBarUM = tokenDetailsUM.topAppBarUM, + collapsedFraction = behavior.state.collapsedFraction, + rootBackground = rootBackground, ) - Box( - modifier = Modifier.hazeEffectTangem { - fallbackTint = HazeTint(rootBackground.copy(alpha = hazeIntensity / 2f)) - progressive = HazeProgressive.verticalGradient( - startIntensity = hazeIntensity, - endIntensity = 0f, - preferPerformance = true, - ) - }, - ) { - TokenDetailsTopBar(topAppBarUM = topAppBarUM) + + if (tokenMarketBlockComponent != null) { + TokenDetailsMarketBlockOverlay( + component = tokenMarketBlockComponent, + rootBackground = rootBackground, + onHeightChange = { marketBlockHeight = it }, + ) } } } +@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 private fun TokenDetailsBody( tokenDetailsUM: TokenDetailsUM, rootBackground: Color, + bottomContentPadding: Dp, modifier: Modifier = Modifier, itemModifier: Modifier = Modifier, ) { - LazyColumn(modifier = modifier) { + LazyColumn( + modifier = modifier, + contentPadding = PaddingValues(bottom = bottomContentPadding), + ) { notifications( notifications = tokenDetailsUM.notifications, contentColor = rootBackground, @@ -150,6 +226,7 @@ private fun TokenDetailsBody( private fun TokenDetailsScreen_Preview() { TangemThemePreviewRedesign { TokenDetailsScreen( + tokenMarketBlockComponent = null, tokenDetailsUM = TokenDetailsUM( topAppBarUM = TokenDetailsTopAppBarUM( titleState = TitleState.WithAccount( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index ee3f598efc..5de00c3eb0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -16,6 +16,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment 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.res.vectorResource 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.action.ActionButtons 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.resolveReference import com.tangem.core.ui.extensions.stringReference @@ -45,13 +50,26 @@ import kotlinx.collections.immutable.persistentListOf private val CurrencyIconSize: Dp = 70.dp private val NetworkBadgeSize: Dp = 24.dp internal val TokenDetailsBalanceBlockHeight: Dp = 404.dp +private const val MIN_SCALE = 0.75f +private const val MAX_SCALE = 1f @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 collapsedFraction = behavior.state.collapsedFraction + val alpha = 1f - collapsedFraction + val scale = alpha.coerceIn(MIN_SCALE, MAX_SCALE) + Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier + .alpha(alpha) + .scale(scale) + .snapToExitUntilCollapsed(behavior) .fillMaxWidth() .padding(vertical = TangemTheme.dimens2.x10), ) { @@ -165,6 +183,7 @@ private fun TokenDetailsBalanceBlock_Preview( TangemThemePreviewRedesign { TokenDetailsBalanceBlock( balanceBlockUM = params, + behavior = rememberTangemExitUntilCollapsedScrollBehavior(), modifier = Modifier.background(TangemTheme.colors2.surface.level2), ) }