Updated on 2026-08-14
This commit is contained in:
parent
128faaec8d
commit
31e0591ef0
20 changed files with 1242 additions and 20 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 = {}) }
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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),
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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()) }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue