From 31e0591ef08c4872d9c2e5556742942eefb401b6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 17:52:48 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../core/ui/ds/button/TangemButtonUM.kt | 2 + .../tangem/core/ui/ds/topbar/TangemTopBar.kt | 11 +- .../collapsing/TangemCollapsingTopBar.kt | 126 +++++++++++ ...BalanceExitUntilCollapsedScrollBehavior.kt | 211 ++++++++++++++++++ .../entity/TangemCollapsingAppBarState.kt | 142 ++++++++++++ ...viewData.kt => WalletPreviewDataLegacy.kt} | 2 +- .../common/preview/WalletScreenPreviewData.kt | 2 +- .../presentation/preview/WalletPreviewData.kt | 22 ++ .../wallet/ui/components/MarketsHint.kt | 89 ++++++++ .../wallet/ui/components/MarketsTooltip.kt | 144 ++++++++++++ .../ui/components/TokenActionsBottomSheet.kt | 6 +- .../wallet/ui/components/WalletItemBlocks.kt | 49 ++++ .../wallet/ui/components/WalletsList.kt | 4 +- .../ui/components/common/WalletBalance.kt | 205 +++++++++++++++++ .../wallet/ui/components/common/WalletCard.kt | 15 +- .../ui/components/common/WalletContent.kt | 67 ++++++ .../components/common/WalletPagerIndicator.kt | 49 ++++ .../ui/components/common/WalletTopBar.kt | 76 ++++++- .../wallet/ui/utils/LazyListStateExt.kt | 31 +++ .../main/res/drawable/ic_magic_default_24.xml | 9 + 20 files changed, 1242 insertions(+), 20 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/TangemCollapsingTopBar.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/WalletBalanceExitUntilCollapsedScrollBehavior.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/entity/TangemCollapsingAppBarState.kt rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/{WalletPreviewData.kt => WalletPreviewDataLegacy.kt} (98%) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletPreviewData.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsHint.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsTooltip.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletPagerIndicator.kt create mode 100644 features/wallet/impl/src/main/res/drawable/ic_magic_default_24.xml diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt index 0b0e426629..a3bb16c4da 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt @@ -1,6 +1,7 @@ package com.tangem.core.ui.ds.button import androidx.annotation.DrawableRes +import androidx.compose.runtime.Stable import com.tangem.core.ui.extensions.TextReference /** @@ -19,6 +20,7 @@ import com.tangem.core.ui.extensions.TextReference * [REDACTED_AUTHOR] */ +@Stable data class TangemButtonUM( val text: TextReference? = null, val descriptionText: TextReference? = null, diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt index b96dd8d788..b38389acc5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt @@ -2,7 +2,7 @@ package com.tangem.core.ui.ds.topbar import android.content.res.Configuration import androidx.annotation.DrawableRes -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon @@ -55,6 +55,7 @@ fun TangemTopBar( modifier = modifier, content = { Column( + modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5), ) { @@ -95,11 +96,17 @@ private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes: AnimatedVisibility( visible = title != null, label = "Title Visibility", + enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(), ) { val wrappedTitle = remember(this) { requireNotNull(title) } Row( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy( + space = TangemTheme.dimens2.x1, + alignment = Alignment.CenterHorizontally, + ), verticalAlignment = Alignment.CenterVertically, ) { AnimatedVisibility( diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/TangemCollapsingTopBar.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/TangemCollapsingTopBar.kt new file mode 100644 index 0000000000..1d91d223b5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/TangemCollapsingTopBar.kt @@ -0,0 +1,126 @@ +package com.tangem.core.ui.ds.topbar.collapsing + +import android.content.res.Configuration +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.DecayAnimationSpec +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import kotlin.math.max +import kotlin.math.roundToInt + + +@Composable +fun TangemCollapsingTopBar( + state: TangemCollapsingAppBarState, + collapsingPart: @Composable () -> Unit, + body: @Composable () -> Unit, +) { + Layout( + modifier = Modifier.fillMaxSize(), + content = { + collapsingPart() + body() + }, + ) { measurables, constraints -> + + val collapsingConstraints = constraints.copy( + minWidth = 0, + minHeight = 0, + ) + val collapsingPlaceable = measurables[0].measure(collapsingConstraints) + + val bodyConstraints = constraints.copy( + minWidth = 0, + minHeight = 0, + maxHeight = (constraints.maxHeight - collapsingConstraints.minHeight).coerceAtLeast(0), + ) + val bodyPlaceable = measurables[1].measure(bodyConstraints) + + val minHeight = 0.dp.roundToPx() + val maxHeight = collapsingPlaceable.height + minHeight + + val offset = state.heightOffset.roundToInt().coerceAtLeast(-maxHeight) + + val width = max( + collapsingPlaceable.width, + bodyPlaceable.width, + ).coerceIn(constraints.minWidth, constraints.maxWidth) + val height = max( + collapsingPlaceable.height, + bodyPlaceable.height, + ).coerceIn(constraints.minHeight, constraints.maxHeight) + + layout(width = width, height = height) { + bodyPlaceable.placeRelative(0, collapsingPlaceable.height + offset) + collapsingPlaceable.placeRelative(0, offset) + } + } +} + +/** + * A scroll behavior for a collapsing top app bar that collapses when scrolling up and expands when scrolling down. + * + * @property state The state of the collapsing app bar. + * @property snapAnimationSpec The animation spec used for snapping the app bar to its collapsed or + * expanded state after a fling. If null, no snapping will occur. + * @property flingAnimationSpec The decay animation spec used for fling gestures. + * If null, fling gestures will not be handled. + * @property nestedScrollConnection Nested scroll connection + */ +@Stable +data class TangemCollapsingAppBarBehavior( + val state: TangemCollapsingAppBarState, + val snapAnimationSpec: AnimationSpec?, + val flingAnimationSpec: DecayAnimationSpec?, + val nestedScrollConnection: NestedScrollConnection, +) + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TangemCollapsingTopBar_Preview() { + TangemThemePreviewRedesign { + val collapsingHeight = 200.dp + val behavior = rememberTangemExitUntilCollapsedScrollBehavior( + expandedHeight = collapsingHeight, + ) + TangemCollapsingTopBar( + state = behavior.state, + collapsingPart = { + Box( + modifier = Modifier + .fillMaxWidth() + .height(collapsingHeight) + .background(Color.Red), + ) + }, + body = { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Blue) + .nestedScroll(behavior.nestedScrollConnection) + .verticalScroll(rememberScrollState()), + ) + }, + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/WalletBalanceExitUntilCollapsedScrollBehavior.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/WalletBalanceExitUntilCollapsedScrollBehavior.kt new file mode 100644 index 0000000000..c3e18a6df6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/WalletBalanceExitUntilCollapsedScrollBehavior.kt @@ -0,0 +1,211 @@ +package com.tangem.core.ui.ds.topbar.collapsing + +import androidx.compose.animation.core.* +import androidx.compose.animation.rememberSplineBasedDecay +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState +import com.tangem.core.ui.ds.topbar.collapsing.entity.TopBapScrollDirection +import com.tangem.core.ui.ds.topbar.collapsing.entity.rememberTangemCollapsingAppBarState +import com.tangem.core.ui.utils.toPx +import kotlin.math.abs +import kotlin.math.absoluteValue + +/** + * A scroll behavior for a collapsing top app bar that collapses when scrolling up and expands when scrolling down. + * When the user stops scrolling, the app bar will settle to either fully collapsed or fully expanded state + * based on the current collapsed fraction and scroll direction. + * + * @param expandedHeight The height of the app bar when it is fully expanded. + * @param partialCollapsedHeight The height of the app bar when it is partially collapsed. + * @param snapAnimationSpec The animation spec for snapping the app bar to the collapsed or expanded state when the + * user stops scrolling. If null, no snapping will occur. + * @param flingAnimationSpec The decay animation spec for the fling behavior when the user flings the app bar. + * If null, no fling behavior will occur. + */ +@Composable +fun rememberTangemExitUntilCollapsedScrollBehavior( + expandedHeight: Dp = -Int.MAX_VALUE.dp, + partialCollapsedHeight: Dp = expandedHeight, + snapAnimationSpec: AnimationSpec? = spring(), + flingAnimationSpec: DecayAnimationSpec? = rememberSplineBasedDecay(), +): TangemCollapsingAppBarBehavior { + val topBarState = rememberTangemCollapsingAppBarState( + heightOffsetLimit = -expandedHeight.toPx(), + partialHeightLimit = partialCollapsedHeight.toPx(), + ) + return exitUntilCollapsedScrollBehavior( + state = topBarState, + snapAnimationSpec = snapAnimationSpec, + flingAnimationSpec = flingAnimationSpec, + ) +} + +/** + * A scroll behavior for a collapsing top app bar that collapses when scrolling up and expands when scrolling down. + * When the user stops scrolling, the app bar will settle to either fully collapsed or fully expanded state + * based on the current collapsed fraction and scroll direction. + * + * @param state The state of the collapsing app bar, which controls the height offset and scroll behavior. + * @param snapAnimationSpec The animation spec for snapping the app bar to the collapsed or expanded state when the + * user stops scrolling. If null, no snapping will occur. + * @param flingAnimationSpec The decay animation spec for the fling behavior when the user flings the app bar. + * If null, no fling behavior will occur. + */ +@Composable +private fun exitUntilCollapsedScrollBehavior( + state: TangemCollapsingAppBarState = rememberTangemCollapsingAppBarState(), + snapAnimationSpec: AnimationSpec? = spring(), + flingAnimationSpec: DecayAnimationSpec? = rememberSplineBasedDecay(), +): TangemCollapsingAppBarBehavior { + val nestedScrollConnection = remember(state) { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val dy = available.y + + val consume = if (dy < 0) { + state.direction = TopBapScrollDirection.Collapsing + state.dispatchRawDelta(dy) + } else { + 0f + } + + return Offset(0f, consume) + } + + override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { + val dy = available.y + + val consume = if (dy > 0) { + state.direction = TopBapScrollDirection.Expanding + state.dispatchRawDelta(dy) + } else { + state.direction = TopBapScrollDirection.Collapsing + 0f + } + + return Offset(0f, consume) + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + val superConsumed = super.onPostFling(consumed, available) + return superConsumed + settleAppBar( + state = state, + velocity = available.y, + flingAnimationSpec = flingAnimationSpec, + snapAnimationSpec = snapAnimationSpec, + ) + } + } + } + + return remember(state, nestedScrollConnection, snapAnimationSpec, flingAnimationSpec) { + TangemCollapsingAppBarBehavior( + state = state, + snapAnimationSpec = snapAnimationSpec, + flingAnimationSpec = flingAnimationSpec, + nestedScrollConnection = nestedScrollConnection, + ) + } +} + +@Composable +fun Modifier.snapToExitUntilCollapsed(behavior: TangemCollapsingAppBarBehavior): Modifier { + return draggable( + orientation = Orientation.Vertical, + state = rememberDraggableState { delta -> + behavior.state.heightOffset += delta + }, + onDragStopped = { velocity -> + settleAppBar( + state = behavior.state, + velocity = velocity, + flingAnimationSpec = behavior.flingAnimationSpec, + snapAnimationSpec = behavior.snapAnimationSpec, + ) + }, + ) +} + +/** + * Settles the app bar to either fully collapsed or fully expanded state + * based on the current collapsed fraction and scroll direction. + */ +@Suppress("MagicNumber", "CyclomaticComplexMethod") +private suspend fun settleAppBar( + state: TangemCollapsingAppBarState, + velocity: Float, + flingAnimationSpec: DecayAnimationSpec?, + snapAnimationSpec: AnimationSpec?, + snapCollapseThreshold: Float = 0.3f, + snapExpandThreshold: Float = 0.7f, +): Velocity { + val partialLimit = state.heightOffsetLimit + state.partialHeightLimit + var remainingVelocity = velocity + + // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar, + // and just return Zero Velocity. + // Note that we don't check for 0f due to float precision with the collapsedFraction + // calculation. + if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) { + return Velocity.Zero + } + + // Fling + if (flingAnimationSpec != null && velocity.absoluteValue > 1f) { + var lastValue = 0f + AnimationState( + initialValue = 0f, + initialVelocity = velocity, + ).animateDecay(flingAnimationSpec) { + val delta = value - lastValue + val initialHeightOffset = state.heightOffset + + val availableDelta = partialLimit - initialHeightOffset + + state.heightOffset = if (delta < 0f && initialHeightOffset > partialLimit) { + (initialHeightOffset + delta).coerceAtLeast(partialLimit) + } else { + initialHeightOffset + delta + } + + val consumed = abs(initialHeightOffset - state.heightOffset) + lastValue = value + remainingVelocity = this.velocity + // avoid rounding errors and stop if anything is unconsumed + if (abs(maxOf(delta, availableDelta) - consumed) > 0.5f) this.cancelAnimation() + } + } + // Snap + if (snapAnimationSpec != null && state.heightOffset > partialLimit && state.heightOffset < 0f) { + AnimationState(initialValue = state.heightOffset).animateTo( + when (state.direction) { + TopBapScrollDirection.Collapsing -> if (state.collapsedFraction > snapCollapseThreshold) { + partialLimit + } else { + 0f + } + TopBapScrollDirection.Expanding -> if (state.collapsedFraction < snapExpandThreshold) { + 0f + } else { + partialLimit + } + TopBapScrollDirection.Idle -> 0f + }, + animationSpec = snapAnimationSpec, + ) { + state.heightOffset = value + } + } + return Velocity(0f, remainingVelocity) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/entity/TangemCollapsingAppBarState.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/entity/TangemCollapsingAppBarState.kt new file mode 100644 index 0000000000..a5d1a8892d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/entity/TangemCollapsingAppBarState.kt @@ -0,0 +1,142 @@ +package com.tangem.core.ui.ds.topbar.collapsing.entity + +import androidx.compose.animation.core.AnimationState +import androidx.compose.animation.core.animateTo +import androidx.compose.animation.core.tween +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.gestures.ScrollScope +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState.Companion.Saver +import kotlin.math.absoluteValue +import kotlin.math.max +import kotlin.math.min + +/** + * State of the collapsing top app bar. + * It contains the current height offset, the limits for collapsing and expanding, and the scroll direction. + * + * @property initialHeightOffset The initial height offset of the app bar. Default is 0f. + * @property heightOffsetLimit The height offset limit for full collapse. + * @property partialHeightLimit The height offset limit for partial collapse. Default is the same as [heightOffsetLimit] + */ +@Stable +class TangemCollapsingAppBarState( + val initialHeightOffset: Float = 0f, + val heightOffsetLimit: Float = 0f, + val partialHeightLimit: Float = heightOffsetLimit, +) : ScrollableState { + + private val _heightOffset = mutableFloatStateOf(initialHeightOffset) + private var deferredConsumption: Float = 0f + + /** + * The current height offset of the app bar. + * This value is updated as the user scrolls, and is constrained between [heightOffsetLimit] and 0f. + */ + var heightOffset: Float + get() = _heightOffset.floatValue + set(newOffset) { + _heightOffset.floatValue = + newOffset.coerceIn(minimumValue = heightOffsetLimit, maximumValue = 0f) + } + + /** + * The fraction of the app bar that is collapsed, calculated as the ratio of [heightOffset] to [heightOffsetLimit]. + */ + val collapsedFraction: Float + get() = + if (heightOffsetLimit != 0f) { + heightOffset / heightOffsetLimit + } else { + 0f + } + + /** + * The current scroll direction of the app bar, which can be Collapsing, Expanding, or Idle. + */ + var direction: TopBapScrollDirection = TopBapScrollDirection.Idle + + private val scrollableState = ScrollableState { value -> + val consume = if (value < 0) { + max(heightOffsetLimit - heightOffset, value) + } else { + min(0f - heightOffset, value) + } + + val current = consume + deferredConsumption + val currentInt = current.toInt() + + if (current.absoluteValue > 0) { + heightOffset += currentInt + deferredConsumption = current - currentInt + } + + consume + } + + override val isScrollInProgress: Boolean + get() = scrollableState.isScrollInProgress + + /** + * + */ + suspend fun collapse() { + AnimationState(initialValue = heightOffset).animateTo( + targetValue = heightOffsetLimit + partialHeightLimit, + animationSpec = tween(), + ) { + heightOffset = value + } + } + + override suspend fun scroll(scrollPriority: MutatePriority, block: suspend ScrollScope.() -> Unit) = + scrollableState.scroll(scrollPriority, block) + + override fun dispatchRawDelta(delta: Float) = scrollableState.dispatchRawDelta(delta) + + companion object { + /** The default [Saver] implementation for [TangemCollapsingAppBarState]. */ + val Saver: Saver = + listSaver( + save = { state -> listOf(state.heightOffsetLimit, state.heightOffset, state.partialHeightLimit) }, + restore = { state -> + TangemCollapsingAppBarState( + heightOffsetLimit = state[0], + partialHeightLimit = state[2], + initialHeightOffset = state[1], + ) + }, + ) + } +} + +/** + * Remembers and saves the state of the collapsing top app bar across recompositions and configuration changes. + */ +@Composable +fun rememberTangemCollapsingAppBarState( + heightOffsetLimit: Float = -Float.MAX_VALUE, + partialHeightLimit: Float = -Float.MAX_VALUE, + initialHeightOffset: Float = 0f, +): TangemCollapsingAppBarState { + return rememberSaveable(saver = Saver) { + TangemCollapsingAppBarState( + initialHeightOffset = initialHeightOffset, + partialHeightLimit = partialHeightLimit, + heightOffsetLimit = heightOffsetLimit, + ) + } +} + +/** + * The scroll direction of the top app bar, which can be Collapsing, Expanding, or Idle. + */ +enum class TopBapScrollDirection { + Collapsing, Expanding, Idle +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt similarity index 98% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt index 1c48f40af5..e613117202 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt @@ -8,7 +8,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @Suppress("LargeClass") -internal object WalletPreviewData { +internal object WalletPreviewDataLegacy { val topBarConfig by lazy { WalletTopBarConfig(onDetailsClick = {}) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index fb85ffc73d..f3019be3a6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -15,7 +15,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarConfig +import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy.topBarConfig import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.utils.StringsSigns.DASH_SIGN import kotlinx.collections.immutable.persistentListOf diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletPreviewData.kt new file mode 100644 index 0000000000..7a61716733 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletPreviewData.kt @@ -0,0 +1,22 @@ +package com.tangem.feature.wallet.presentation.preview + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletActionButtons +import kotlinx.collections.immutable.persistentListOf + +internal object WalletPreviewData { + + val wallets by lazy { + mapOf( + UserWalletId(stringValue = "123") to WalletBalancePreview.content, + UserWalletId(stringValue = "321") to WalletBalancePreview.loading, + UserWalletId(stringValue = "24") to WalletBalancePreview.error, + ) + } + + val actionButtons = persistentListOf( + WalletActionButtons.Buy({}, false).buttonUM, + WalletActionButtons.Swap({}, false).buttonUM, + WalletActionButtons.Sell({}, false).buttonUM, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsHint.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsHint.kt new file mode 100644 index 0000000000..32c059a463 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsHint.kt @@ -0,0 +1,89 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.feature.wallet.impl.R + +private const val STARS_INLINE_CONTENT_ID = "stars" + +@Composable +internal fun MarketsHint(isVisible: Boolean, modifier: Modifier = Modifier) { + AnimatedVisibility( + modifier = modifier, + visible = isVisible, + enter = fadeIn(animationSpec = tween(durationMillis = 300)), + exit = fadeOut(animationSpec = tween(durationMillis = 300)), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Swipe up to explore the market", // todo redesign main lokalise + style = TangemTheme.typography2.bodyRegular14, + color = TangemTheme.colors2.text.neutral.primary, + textAlign = TextAlign.Center, + ) + Text( + text = buildAnnotatedString { + append("Find new hidden gems ") // todo redesign main lokalise + appendInlineContent( + STARS_INLINE_CONTENT_ID, + alternateText = "\uDBC0\uDDBF", + ) + }, + inlineContent = mapOf( + STARS_INLINE_CONTENT_ID to InlineTextContent( + placeholder = Placeholder( + width = TangemTheme.typography2.bodyRegular14.fontSize, + height = TangemTheme.typography2.bodyRegular14.fontSize, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center, + ), + children = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_magic_default_24), + tint = TangemTheme.colors2.text.neutral.tertiary, + contentDescription = null, + ) + }, + ), + ), + style = TangemTheme.typography2.bodyRegular14, + color = TangemTheme.colors2.text.neutral.tertiary, + textAlign = TextAlign.Center, + ) + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun MarketsHint_Preview() { + TangemThemePreviewRedesign { + MarketsHint( + isVisible = true, + ) + } +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsTooltip.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsTooltip.kt new file mode 100644 index 0000000000..7eeadda4db --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsTooltip.kt @@ -0,0 +1,144 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideIn +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.* +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.sheetscaffold.TangemSheetState +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.test.MarketTooltipTestTags +import com.tangem.core.ui.utils.lineTo +import com.tangem.core.ui.utils.moveTo +import com.tangem.core.ui.utils.toPx +import com.tangem.feature.wallet.impl.R +import kotlinx.coroutines.delay +import kotlin.math.roundToInt + +@Composable +internal fun MarketsTooltip( + availableHeight: Dp, + bottomSheetState: TangemSheetState, + isVisible: Boolean, + modifier: Modifier = Modifier, +) { + val density = LocalDensity.current + val tooltipOffset by remember { + derivedStateOf { + val bottomSheetOffset = try { + // Can throw exception during the first composition + with(density) { bottomSheetState.requireOffset().toDp() } + } catch (e: Exception) { + 0.dp + } + + bottomSheetOffset - availableHeight + } + } + + var isVisibleWrapped by remember { mutableStateOf(value = false) } + LaunchedEffect(isVisible) { + if (isVisible) { + delay(timeMillis = 300) + } + + isVisibleWrapped = isVisible + } + + val slideOffset = 40.dp.toPx() + AnimatedVisibility( + modifier = modifier + .offset { IntOffset(x = 0, y = tooltipOffset.roundToPx()) } + .testTag(MarketTooltipTestTags.CONTAINER), + visible = isVisibleWrapped, + enter = slideIn( + animationSpec = spring( + stiffness = Spring.StiffnessLow, + visibilityThreshold = IntOffset.VisibilityThreshold, + ), + initialOffset = { _ -> IntOffset(y = -slideOffset.roundToInt(), x = 0) }, + ) + fadeIn(), + exit = fadeOut(), + ) { + MarketsTooltipContent() + } +} + +@Composable +internal fun MarketsTooltipContent(modifier: Modifier = Modifier) { + val backgroundColor = TangemTheme.colors.background.action + val cornerRadius = CornerRadius(x = 14.dp.toPx()) + val tipDpSize = DpSize(width = 20.dp, height = 8.dp) + + Column( + modifier = modifier + .padding(bottom = tipDpSize.height) + .drawBehind { + val rect = size.toRect() + val tipSize = tipDpSize.toSize() + val tipRect = Rect( + offset = Offset( + x = rect.center.x - tipSize.center.x, + y = rect.bottom, + ), + size = tipSize, + ) + drawRoundRect(color = backgroundColor, cornerRadius = cornerRadius) + + val tipPath = Path().apply { + moveTo(tipRect.topLeft) + lineTo(tipRect.bottomCenter) + lineTo(tipRect.topRight) + } + drawPath(color = backgroundColor, path = tipPath) + } + .padding(all = 12.dp), + verticalArrangement = Arrangement.spacedBy(space = 4.dp), + horizontalAlignment = Alignment.Start, + ) { + Text( + text = stringResourceSafe(id = R.string.markets_tooltip_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = stringResourceSafe(id = R.string.markets_tooltip_message), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun MarketsTooltip_Preview() { + TangemThemePreviewRedesign { + MarketsTooltipContent() + } +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt index ea93bb097c..6916bf40c8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt @@ -9,14 +9,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.SimpleSettingsRow -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.getDefaultRowColors import com.tangem.core.ui.components.getWarningRowColors import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy import com.tangem.feature.wallet.presentation.wallet.state.model.ActionsBottomSheetConfig import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonConfig import kotlinx.collections.immutable.ImmutableList @@ -64,5 +64,5 @@ private fun ActionsBottomSheetContent_Light( } private class ActionsBottomSheetContentConfigProvider : CollectionPreviewParameterProvider( - collection = listOf(WalletPreviewData.actionsBottomSheet), + collection = listOf(WalletPreviewDataLegacy.actionsBottomSheet), ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt new file mode 100644 index 0000000000..443bd5f959 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import com.tangem.core.ui.ds.button.TangemButton +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM +import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPayMainScreenBlock + +internal fun LazyListScope.nftCollections2(state: WalletUM, itemModifier: Modifier) { + (state as? WalletUM.Content)?.let { content -> + item(key = "NFTCollections", contentType = "NFTCollections") { + WalletNFTItem( + modifier = itemModifier, + state = content.nftState, + ) + } + } +} + +internal fun LazyListScope.organizeTokens2(state: WalletUM, itemModifier: Modifier) { + val organizeButton = state.tokensListUM.organizeButtonUM + if (organizeButton != null) { + item( + key = "OrganizeTokensButton", + contentType = "OrganizeTokensButton", + ) { + TangemButton( + organizeButton, + modifier = itemModifier, + ) + } + } +} + +internal fun LazyListScope.tangemPay(walletUM: WalletUM, isBalanceHiding: Boolean, modifier: Modifier = Modifier) { + if (walletUM is WalletState.MultiCurrency) { + item( + key = "TangemPayMainScreenBlock", + contentType = walletUM.tangemPayState::class.java, + ) { + TangemPayMainScreenBlock( + state = walletUM.tangemPayState, + isBalanceHidden = isBalanceHiding, + modifier = modifier, + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index bddfb23fb1..2febad643b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -26,7 +26,7 @@ import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.MainScreenTestTags -import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletCard import kotlinx.collections.immutable.ImmutableList @@ -106,7 +106,7 @@ private fun Preview_WalletsList() { TangemThemePreview { WalletsList( lazyListState = rememberLazyListState(), - wallets = WalletPreviewData.wallets.values.toPersistentList(), + wallets = WalletPreviewDataLegacy.wallets.values.toPersistentList(), isBalanceHidden = false, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt new file mode 100644 index 0000000000..87d9cebc62 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt @@ -0,0 +1,205 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.common + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.TextAutoSize +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +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.platform.testTag +import androidx.compose.ui.res.vectorResource +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 com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.text.applyBladeBrush +import com.tangem.core.ui.ds.button.SecondaryTangemButton +import com.tangem.core.ui.ds.button.TangemButtonShape +import com.tangem.core.ui.ds.button.TangemButtonUM +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.orEmpty +import com.tangem.core.ui.extensions.orMaskWithStars +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.core.ui.test.MainScreenTestTags +import com.tangem.feature.wallet.presentation.preview.WalletBalancePreview +import com.tangem.feature.wallet.presentation.preview.WalletPreviewData +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM +import com.tangem.feature.wallet.presentation.wallet.ui.components.fastForEach +import kotlinx.collections.immutable.ImmutableList + +private const val MIN_SCALE = 0.75f +private const val MAX_SCALE = 1f + +@Composable +internal fun WalletBalance( + walletBalanceUM: WalletBalanceUM, + behavior: TangemCollapsingAppBarBehavior, + buttons: ImmutableList, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + 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(top = 64.dp) + .statusBarsPadding(), + ) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + ) { + Balance( + walletBalanceUM = walletBalanceUM, + isBalanceHidden = isBalanceHidden, + ) + SpacerH(TangemTheme.dimens2.x3) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), + ) { + Text( + text = walletBalanceUM.name, + style = TangemTheme.typography2.bodyRegular14, + color = TangemTheme.colors2.text.neutral.tertiary, + ) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant, + modifier = Modifier.size(TangemTheme.dimens2.x6), + ) + } + } + SpacerH(TangemTheme.dimens2.x2) + ActionButtons(buttons) + SpacerH(TangemTheme.dimens2.x6) + } +} + +@Composable +private fun Balance(walletBalanceUM: WalletBalanceUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { + AnimatedContent( + targetState = walletBalanceUM, + label = "Update the balance", + modifier = modifier.testTag(MainScreenTestTags.WALLET_BALANCE), + transitionSpec = { + fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) togetherWith + fadeOut(animationSpec = tween(durationMillis = 90)) + }, + ) { balanceUM -> + when (balanceUM) { + is WalletBalanceUM.Content -> { + Text( + text = balanceUM.balance.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), + style = TangemTheme.typography2.titleRegular44.applyBladeBrush( + isEnabled = balanceUM.isBalanceFlickering, + textColor = TangemTheme.colors2.text.neutral.primary, + ), + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + autoSize = TextAutoSize.StepBased( + minFontSize = TangemTheme.typography2.bodySemibold15.fontSize, + maxFontSize = TangemTheme.typography2.titleRegular44.fontSize, + ), + ) + } + is WalletBalanceUM.Error, + is WalletBalanceUM.Loading, + -> { + TextShimmer( + text = "123456", + style = TangemTheme.typography2.titleRegular44, + radius = TangemTheme.dimens2.x25, + textSizeHeight = true, + ) + } + } + } +} + +@Composable +private fun ActionButtons(buttons: ImmutableList) { + Row( + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + buttons.fastForEach { button -> + key(button.text) { + Column( + modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x2_5), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SecondaryTangemButton( + iconRes = button.iconRes, + onClick = button.onClick, + shape = TangemButtonShape.Rounded, + ) + Text( + text = button.text.orEmpty().resolveReference(), + style = TangemTheme.typography2.bodySemibold15, + color = TangemTheme.colors2.text.neutral.primary, + ) + } + } + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun WalletBalance_Preview(@PreviewParameter(WalletBalancePreviewProvider::class) params: WalletBalanceUM) { + TangemThemePreviewRedesign { + WalletBalance( + walletBalanceUM = params, + behavior = rememberTangemExitUntilCollapsedScrollBehavior(), + buttons = WalletPreviewData.actionButtons, + isBalanceHidden = false, + ) + } +} + +private class WalletBalancePreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + WalletBalancePreview.content, + WalletBalancePreview.content.copy(isBalanceFlickering = true), + WalletBalancePreview.loading, + WalletBalancePreview.error, + ) +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 01044070d2..651707a1c3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.* -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.TextAutoSize import androidx.compose.material3.* @@ -45,7 +44,7 @@ import com.tangem.core.ui.res.TangemDimens import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.MainScreenTestTags -import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDropDownItems @@ -375,22 +374,22 @@ private fun Preview_WalletCard( private class WalletCardStateProvider : CollectionPreviewParameterProvider( collection = listOf( - WalletPreviewData.walletCardContentState, - WalletPreviewData.walletCardContentState.copy( + WalletPreviewDataLegacy.walletCardContentState, + WalletPreviewDataLegacy.walletCardContentState.copy( balance = "0.00", ), - WalletPreviewData.walletCardContentState.copy( + WalletPreviewDataLegacy.walletCardContentState.copy( title = "Title", additionalInfo = WalletAdditionalInfo( hideable = false, content = TextReference.Str("3 cards"), ), ), - WalletPreviewData.walletCardContentState.copy( + WalletPreviewDataLegacy.walletCardContentState.copy( isBalanceFlickering = true, ), - WalletPreviewData.walletCardLoadingState, - WalletPreviewData.walletCardErrorState, + WalletPreviewDataLegacy.walletCardLoadingState, + WalletPreviewDataLegacy.walletCardErrorState, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt index 5620f17592..066a167e8d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt @@ -1,12 +1,79 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.rememberOverscrollEffect +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.paging.compose.LazyPagingItems +import com.tangem.common.ui.notifications.notifications +import com.tangem.common.ui.notifications.notificationsCarousel import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.txHistoryItems +import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.tokensListItems +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.tokensListItems2 +import com.tangem.feature.wallet.presentation.wallet.ui.components.nftCollections2 +import com.tangem.feature.wallet.presentation.wallet.ui.components.organizeTokens2 +import com.tangem.feature.wallet.presentation.wallet.ui.components.tangemPay +import kotlinx.collections.immutable.toPersistentList + +@Composable +internal fun WalletListContent( + currentWallet: WalletUM, + isBalanceHidden: Boolean, + listState: LazyListState, + contentPadding: PaddingValues, + modifier: Modifier = Modifier, +) { + val containerColor = TangemTheme.colors2.surface.level1 + + val movableItemModifier = Modifier.padding(horizontal = TangemTheme.dimens2.x3) + val itemModifier = movableItemModifier.padding(top = TangemTheme.dimens2.x3) + + LazyColumn( + modifier = modifier, + state = listState, + contentPadding = contentPadding, + horizontalAlignment = Alignment.CenterHorizontally, + overscrollEffect = rememberOverscrollEffect(), + ) { + notifications( + notifications = currentWallet.notifications.map { it.messageUM } + .toPersistentList(), + contentColor = containerColor, + modifier = movableItemModifier, + ) + notificationsCarousel( + containerColor = containerColor, + modifier = movableItemModifier, + notifications = currentWallet.notifications.map { it.messageUM } + .toPersistentList(), + ) + + tangemPay( + walletUM = currentWallet, + isBalanceHiding = isBalanceHidden, + modifier = itemModifier, + ) + + tokensListItems2( + walletTokensListUM = currentWallet.tokensListUM, + modifier = movableItemModifier, + isBalanceHidden = isBalanceHidden, + ) + + nftCollections2(state = currentWallet, itemModifier = itemModifier) + + organizeTokens2(state = currentWallet, itemModifier = itemModifier) + } +} /** * Wallet content diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletPagerIndicator.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletPagerIndicator.kt new file mode 100644 index 0000000000..d047533bb3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletPagerIndicator.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.common + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.ds.TangemPagerIndicator +import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior + +private const val MIN_SCALE = 0.75f +private const val MAX_SCALE = 1f + +@Composable +internal fun WalletPagerIndicator(pagerState: PagerState, behavior: TangemCollapsingAppBarBehavior) { + val collapsedFraction = behavior.state.collapsedFraction + val alpha = MAX_SCALE - collapsedFraction + val scale = alpha.coerceIn(MIN_SCALE, MAX_SCALE) + + Box( + modifier = Modifier + .graphicsLayer { + scaleY = scale + translationY = behavior.state.heightOffset + } + .fillMaxWidth() + .height( + with(LocalDensity.current) { + behavior.state.heightOffsetLimit.toDp().unaryMinus() + }, + ) + .alpha(alpha), + ) { + TangemPagerIndicator( + pagerState = pagerState, + modifier = Modifier + .padding(top = 248.dp) + .scale(scaleY = 1f, scaleX = scale) + .fillMaxWidth(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt index cb76a6809e..8438399294 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt @@ -3,22 +3,77 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common import android.content.res.Configuration import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.ds.topbar.TangemTopBar +import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior +import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.LocalPowerSavingState 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.test.MainScreenTestTags import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTopBarConfig +import dev.chrisbanes.haze.HazeProgressive + +private const val VISIBILITY_THRESHOLD = 0.5f + +/** + * Wallet screen collapsing top bar + * + * @param topBarConfig top bar config + * @param walletBalance wallet balance text reference + * @param behavior collapsing behavior + */ +@Composable +internal fun WalletTopBar( + topBarConfig: WalletTopBarConfig, + walletBalance: TextReference?, + behavior: TangemCollapsingAppBarBehavior, +) { + Surface( + color = Color.Unspecified, + contentColor = Color.Unspecified, + modifier = Modifier.hazeEffectTangem { + progressive = + HazeProgressive.verticalGradient(startIntensity = 1f, endIntensity = 0f) + }, + ) { + val isPowerSaving by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsStateWithLifecycle() + + val wrappedBalance = remember(behavior.state.collapsedFraction) { + if (behavior.state.collapsedFraction > VISIBILITY_THRESHOLD) walletBalance else null + } + + TangemTopBar( + title = wrappedBalance, + startIconRes = R.drawable.ic_tangem_24, + endIconRes = R.drawable.ic_more_default_24, + onEndContentClick = topBarConfig.onDetailsClick, + isGhostButtons = !isPowerSaving, + modifier = Modifier + .testTag(MainScreenTestTags.TOP_BAR), + ) + } +} /** * Wallet screen top bar * * @param config component config */ +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun WalletTopBar(config: WalletTopBarConfig) { @@ -46,6 +101,21 @@ internal fun WalletTopBar(config: WalletTopBarConfig) { @Composable private fun Preview_WalletTopBar() { TangemThemePreview { - WalletTopBar(config = WalletPreviewData.topBarConfig) + WalletTopBar(config = WalletPreviewDataLegacy.topBarConfig) } -} \ 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 WalletTopBar_Preview() { + TangemThemePreviewRedesign { + WalletTopBar( + topBarConfig = WalletTopBarConfig(onDetailsClick = {}), + walletBalance = stringReference("$ 8923,05"), + behavior = rememberTangemExitUntilCollapsedScrollBehavior(), + ) + } +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt index d1b702c3b9..d4ff2fd0cb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt @@ -4,6 +4,9 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.lazy.LazyListLayoutInfo import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.mapSaver +import com.tangem.utils.extensions.mapNotNullValues /** * Animate scroll [LazyListState]. @@ -28,4 +31,32 @@ private fun calculateOffset(layoutInfo: LazyListLayoutInfo, prevIndex: Int, newI private fun LazyListLayoutInfo.getItemSizeWithSpacing(): Int { return viewportSize.width - afterContentPadding - beforeContentPadding + mainAxisItemSpacing +} + +/** + * Saver for [LazyListState] map, where key is page index, and value is [LazyListState] of this page. + */ +internal fun lazyListStateMapSaver(pageCount: Int): Saver, Any> { + return mapSaver( + save = { map -> + map.mapKeys { it.key.toString() } + .mapValues { listState -> + listState.value.firstVisibleItemIndex to listState.value.firstVisibleItemScrollOffset + } + }, + restore = { restoredMap -> + @Suppress("UNCHECKED_CAST") + val typedMap = restoredMap as? Map> ?: return@mapSaver null + + typedMap.mapKeys { it.key.toInt() } + .mapNotNullValues { (_, value) -> + val (index, offset) = value + LazyListState(index, offset) + } + .toMutableMap() + .apply { + repeat(pageCount) { putIfAbsent(it, LazyListState()) } + } + }, + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/res/drawable/ic_magic_default_24.xml b/features/wallet/impl/src/main/res/drawable/ic_magic_default_24.xml new file mode 100644 index 0000000000..62d66ecd28 --- /dev/null +++ b/features/wallet/impl/src/main/res/drawable/ic_magic_default_24.xml @@ -0,0 +1,9 @@ + + +