Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-27 17:52:48 +05:00
parent 128faaec8d
commit 31e0591ef0
20 changed files with 1242 additions and 20 deletions

View file

@ -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,

View file

@ -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(

View file

@ -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<Float>?,
val flingAnimationSpec: DecayAnimationSpec<Float>?,
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

View file

@ -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<Float>? = spring(),
flingAnimationSpec: DecayAnimationSpec<Float>? = 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<Float>? = spring(),
flingAnimationSpec: DecayAnimationSpec<Float>? = 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<Float>?,
snapAnimationSpec: AnimationSpec<Float>?,
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)
}

View file

@ -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<TangemCollapsingAppBarState, *> =
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
}

View file

@ -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 = {}) }

View file

@ -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

View file

@ -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,
)
}

View file

@ -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

View file

@ -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

View file

@ -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<ActionsBottomSheetConfig>(
collection = listOf(WalletPreviewData.actionsBottomSheet),
collection = listOf(WalletPreviewDataLegacy.actionsBottomSheet),
)

View file

@ -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,
)
}
}
}

View file

@ -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,
)
}

View file

@ -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<TangemButtonUM>,
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<TangemButtonUM>) {
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<WalletBalanceUM> {
override val values: Sequence<WalletBalanceUM>
get() = sequenceOf(
WalletBalancePreview.content,
WalletBalancePreview.content.copy(isBalanceFlickering = true),
WalletBalancePreview.loading,
WalletBalancePreview.error,
)
}
// endregion

View file

@ -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<WalletCardState>(
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,
),
)

View file

@ -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

View file

@ -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(),
)
}
}

View file

@ -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)
}
}
}
// 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

View file

@ -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<MutableMap<Int, LazyListState>, 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<String, Pair<Int, Int>> ?: return@mapSaver null
typedMap.mapKeys { it.key.toInt() }
.mapNotNullValues { (_, value) ->
val (index, offset) = value
LazyListState(index, offset)
}
.toMutableMap()
.apply {
repeat(pageCount) { putIfAbsent(it, LazyListState()) }
}
},
)
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M11.516,6.27C11.403,6.27 11.343,6.2 11.325,6.095C11.066,4.674 11.092,4.604 9.587,4.332C9.475,4.306 9.414,4.245 9.414,4.131C9.414,4.025 9.475,3.955 9.587,3.938C11.092,3.666 11.066,3.596 11.325,2.175C11.343,2.07 11.403,2 11.516,2C11.628,2 11.689,2.07 11.706,2.175C11.965,3.596 11.939,3.666 13.444,3.938C13.548,3.955 13.617,4.025 13.617,4.131C13.617,4.245 13.548,4.306 13.444,4.332C11.939,4.604 11.965,4.674 11.706,6.095C11.689,6.2 11.628,6.27 11.516,6.27ZM7.33,12.302C7.174,12.302 7.062,12.189 7.036,12.013C6.759,9.672 6.646,9.611 4.294,9.225C4.112,9.199 4,9.102 4,8.927C4,8.769 4.112,8.664 4.259,8.637C6.629,8.182 6.759,8.19 7.036,5.849C7.062,5.674 7.174,5.56 7.33,5.56C7.494,5.56 7.606,5.674 7.624,5.84C7.926,8.217 8.013,8.295 10.4,8.637C10.547,8.655 10.66,8.769 10.66,8.927C10.66,9.093 10.547,9.199 10.4,9.225C8.013,9.69 7.935,9.69 7.624,12.031C7.606,12.189 7.494,12.302 7.33,12.302ZM13.193,22C12.969,22 12.804,21.833 12.77,21.597C12.121,16.801 11.472,16.16 6.802,15.538C6.551,15.512 6.387,15.336 6.387,15.099C6.387,14.872 6.551,14.696 6.802,14.67C11.481,14.144 12.156,13.407 12.77,8.602C12.804,8.366 12.969,8.208 13.193,8.208C13.418,8.208 13.583,8.366 13.626,8.602C14.24,13.407 14.906,14.144 19.594,14.67C19.836,14.696 20,14.872 20,15.099C20,15.336 19.836,15.512 19.594,15.538C14.906,16.064 14.24,16.801 13.626,21.597C13.583,21.833 13.418,22 13.193,22Z"
android:fillColor="#000000"/>
</vector>