Updated on 2026-08-14
This commit is contained in:
parent
9112d2bab8
commit
890048f5b2
9 changed files with 194 additions and 57 deletions
|
|
@ -9,6 +9,7 @@ import androidx.compose.material.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
|
@ -77,12 +78,13 @@ private fun Button(
|
|||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clickable(enabled = config.enabled, onClick = config.onClick)
|
||||
.heightIn(min = TangemTheme.dimens.size36)
|
||||
.clip(shape)
|
||||
.background(
|
||||
color = if (config.enabled) color else TangemTheme.colors.button.disabled,
|
||||
shape = shape,
|
||||
)
|
||||
.clickable(enabled = config.enabled, onClick = config.onClick)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing24,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -35,6 +36,11 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
fun Notification(state: NotificationState, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius18))
|
||||
.background(
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
shape = RoundedCornerShape(size = TangemTheme.dimens.radius18),
|
||||
)
|
||||
.clickable(
|
||||
enabled = when (state) {
|
||||
is NotificationState.Simple -> false
|
||||
|
|
@ -45,10 +51,6 @@ fun Notification(state: NotificationState, modifier: Modifier = Modifier) {
|
|||
} else {
|
||||
{}
|
||||
},
|
||||
)
|
||||
.background(
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
RoundedCornerShape(size = TangemTheme.dimens.radius18),
|
||||
),
|
||||
) {
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ internal object WalletPreviewData {
|
|||
balance = "8923,05 $",
|
||||
additionalInfo = "3 cards • Seed enabled",
|
||||
imageResId = R.drawable.ill_businessman_3d,
|
||||
onClick = {},
|
||||
onClick = null,
|
||||
)
|
||||
|
||||
val walletCardLoadingState = WalletCardState.Loading(
|
||||
|
|
@ -30,7 +30,7 @@ internal object WalletPreviewData {
|
|||
title = "Wallet 1",
|
||||
additionalInfo = "3 cards • Seed enabled",
|
||||
imageResId = R.drawable.ill_businessman_3d,
|
||||
onClick = {},
|
||||
onClick = null,
|
||||
)
|
||||
|
||||
val walletCardHiddenContentState = WalletCardState.HiddenContent(
|
||||
|
|
@ -38,7 +38,7 @@ internal object WalletPreviewData {
|
|||
title = "Wallet 1",
|
||||
additionalInfo = "3 cards • Seed enabled",
|
||||
imageResId = R.drawable.ill_businessman_3d,
|
||||
onClick = {},
|
||||
onClick = null,
|
||||
)
|
||||
|
||||
val walletCardErrorState = WalletCardState.Error(
|
||||
|
|
@ -46,7 +46,7 @@ internal object WalletPreviewData {
|
|||
title = "Wallet 1",
|
||||
additionalInfo = "3 cards • Seed enabled",
|
||||
imageResId = R.drawable.ill_businessman_3d,
|
||||
onClick = {},
|
||||
onClick = null,
|
||||
)
|
||||
|
||||
val walletListConfig = WalletsListConfig(
|
||||
|
|
@ -288,12 +288,7 @@ internal object WalletPreviewData {
|
|||
),
|
||||
),
|
||||
),
|
||||
notifications = persistentListOf(
|
||||
WalletNotification.UnreachableNetworks,
|
||||
WalletNotification.LikeTangemApp(onClick = {}),
|
||||
WalletNotification.NeedToBackup(onClick = {}),
|
||||
WalletNotification.ScanCard(onClick = {}),
|
||||
),
|
||||
notifications = persistentListOf(WalletNotification.LikeTangemApp(onClick = {})),
|
||||
buttons = manageButtons,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
|
|
@ -27,18 +25,19 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
*/
|
||||
@Composable
|
||||
internal fun WalletManageButtons(buttons: ImmutableList<WalletManageButton>, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
LazyRow(
|
||||
modifier = modifier
|
||||
.horizontalScroll(state = rememberScrollState())
|
||||
.background(color = TangemTheme.colors.background.secondary)
|
||||
.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16),
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
buttons.forEach { button ->
|
||||
key(button.config.text) {
|
||||
ActionButton(config = button.config)
|
||||
}
|
||||
}
|
||||
items(
|
||||
items = buttons,
|
||||
key = { it.config.text },
|
||||
itemContent = { ActionButton(config = it.config) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -31,6 +33,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletTopBar
|
|||
import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletContentItemDecoration
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletNotificationDecoration
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
|
||||
|
||||
/**
|
||||
* Wallet screen
|
||||
|
|
@ -39,6 +42,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletNotifi
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun WalletScreen(state: WalletStateHolder) {
|
||||
BackHandler(onBack = state.onBackClick)
|
||||
|
|
@ -48,6 +52,9 @@ internal fun WalletScreen(state: WalletStateHolder) {
|
|||
containerColor = TangemTheme.colors.background.secondary,
|
||||
) { scaffoldPaddings ->
|
||||
|
||||
val walletsListState = rememberLazyListState()
|
||||
val changeableItemModifier = Modifier.changeWalletAnimator(walletsListState)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues = scaffoldPaddings)
|
||||
|
|
@ -58,6 +65,7 @@ internal fun WalletScreen(state: WalletStateHolder) {
|
|||
item {
|
||||
WalletsList(
|
||||
config = state.walletsListConfig,
|
||||
lazyListState = walletsListState,
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing14),
|
||||
)
|
||||
}
|
||||
|
|
@ -66,9 +74,7 @@ internal fun WalletScreen(state: WalletStateHolder) {
|
|||
item {
|
||||
WalletManageButtons(
|
||||
buttons = state.buttons,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.padding(bottom = TangemTheme.dimens.spacing14),
|
||||
modifier = changeableItemModifier.padding(bottom = TangemTheme.dimens.spacing14),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -78,7 +84,7 @@ internal fun WalletScreen(state: WalletStateHolder) {
|
|||
itemContent = { index, item ->
|
||||
Notification(
|
||||
state = item.state,
|
||||
modifier = Modifier.walletNotificationDecoration(
|
||||
modifier = changeableItemModifier.walletNotificationDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = state.notifications.lastIndex,
|
||||
),
|
||||
|
|
@ -100,7 +106,7 @@ internal fun WalletScreen(state: WalletStateHolder) {
|
|||
itemContent = { index, item ->
|
||||
ContentItem(
|
||||
item = item,
|
||||
modifier = Modifier.walletContentItemDecoration(
|
||||
modifier = changeableItemModifier.walletContentItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = state.contentItems.lastIndex,
|
||||
),
|
||||
|
|
@ -109,7 +115,14 @@ internal fun WalletScreen(state: WalletStateHolder) {
|
|||
)
|
||||
|
||||
if (state is WalletStateHolder.MultiCurrencyContent) {
|
||||
item { OrganizeTokensButton(onClick = state.onOrganizeTokensClick) }
|
||||
item {
|
||||
OrganizeTokensButton(
|
||||
onClick = state.onOrganizeTokensClick,
|
||||
modifier = changeableItemModifier
|
||||
.padding(top = TangemTheme.dimens.spacing14)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -195,16 +208,14 @@ private fun TransactionGroupTitle(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun OrganizeTokensButton(onClick: () -> Unit) {
|
||||
private fun OrganizeTokensButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
RoundedActionButton(
|
||||
config = ActionButtonConfig(
|
||||
text = stringResource(id = R.string.organize_tokens_title),
|
||||
iconResId = R.drawable.ic_filter_24,
|
||||
onClick = onClick,
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
|
|
@ -20,11 +21,8 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletCardState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletsListConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector
|
||||
import kotlinx.coroutines.InternalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
|
||||
private const val MIN_OFFSET = -2
|
||||
private const val MAX_OFFSET = 2
|
||||
|
||||
/**
|
||||
* Wallets list component
|
||||
|
|
@ -36,11 +34,10 @@ private const val MAX_OFFSET = 2
|
|||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class, InternalCoroutinesApi::class)
|
||||
@Composable
|
||||
internal fun WalletsList(config: WalletsListConfig, modifier: Modifier = Modifier) {
|
||||
internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState, modifier: Modifier = Modifier) {
|
||||
val horizontalCardPadding = TangemTheme.dimens.spacing16
|
||||
val itemWidth = LocalConfiguration.current.screenWidthDp.dp - horizontalCardPadding * 2
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
LazyRow(
|
||||
modifier = modifier.background(color = TangemTheme.colors.background.secondary),
|
||||
state = lazyListState,
|
||||
|
|
@ -54,17 +51,8 @@ internal fun WalletsList(config: WalletsListConfig, modifier: Modifier = Modifie
|
|||
}
|
||||
|
||||
LaunchedEffect(lazyListState) {
|
||||
snapshotFlow(lazyListState::firstVisibleItemScrollOffset)
|
||||
.collect(
|
||||
collector = FlowCollector { firstVisibleItemScrollOffset ->
|
||||
val itemSize = lazyListState.layoutInfo.visibleItemsInfo.firstOrNull()?.size ?: 1
|
||||
val offset = lazyListState.firstVisibleItemIndex * itemSize + firstVisibleItemScrollOffset
|
||||
val currentIndex = offset / itemSize
|
||||
if (offset - currentIndex * itemSize in MIN_OFFSET..MAX_OFFSET) {
|
||||
config.onWalletChange(currentIndex)
|
||||
}
|
||||
},
|
||||
)
|
||||
snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo }
|
||||
.collect(collector = ScrollOffsetCollector(callback = config.onWalletChange))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +60,10 @@ internal fun WalletsList(config: WalletsListConfig, modifier: Modifier = Modifie
|
|||
@Composable
|
||||
private fun Preview_WalletHeader_LightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
WalletsList(config = WalletPreviewData.walletListConfig)
|
||||
WalletsList(
|
||||
config = WalletPreviewData.walletListConfig,
|
||||
lazyListState = rememberLazyListState(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +71,9 @@ private fun Preview_WalletHeader_LightTheme() {
|
|||
@Composable
|
||||
private fun Preview_WalletHeader_DarkTheme() {
|
||||
TangemTheme(isDark = true) {
|
||||
WalletsList(config = WalletPreviewData.walletListConfig)
|
||||
WalletsList(
|
||||
config = WalletPreviewData.walletListConfig,
|
||||
lazyListState = rememberLazyListState(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui.utils
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Flow collector for scroll items tracking.
|
||||
* If first visible item offset is greater than half item size, then [callback] be invoked.
|
||||
* If last visible item offset is greater than half item size, then [callback] be invoked.
|
||||
*
|
||||
* @property callback lambda be invoked when current scroll items is changed
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class ScrollOffsetCollector(
|
||||
private val callback: (Int) -> Unit,
|
||||
) : FlowCollector<List<LazyListItemInfo>> {
|
||||
|
||||
private val LazyListItemInfo.halfItemSize get() = size.div(other = 2)
|
||||
|
||||
override suspend fun emit(value: List<LazyListItemInfo>) {
|
||||
val firstItem = requireNotNull(value.firstOrNull()) {
|
||||
"At least 1 item should be visible in list"
|
||||
}
|
||||
|
||||
val lastItem = requireNotNull(value.lastOrNull()) {
|
||||
"At least 1 item should be visible in list"
|
||||
}
|
||||
|
||||
if (abs(firstItem.offset) > firstItem.halfItemSize) {
|
||||
callback(firstItem.index + 1)
|
||||
} else if (abs(lastItem.offset) > lastItem.halfItemSize) {
|
||||
callback(lastItem.index - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui.utils
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.absoluteOffset
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
private const val ANIMATION_MAX_OFFSET = 80
|
||||
private const val ANIMATION_MAX_ALPHA = 1f
|
||||
|
||||
/**
|
||||
* Modifier extension for setting [absoluteOffset] and [alpha] animations of wallet change
|
||||
*
|
||||
* @param lazyListState lazy list state
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal fun Modifier.changeWalletAnimator(lazyListState: LazyListState) = composed {
|
||||
val walletItemOffset by remember(lazyListState) { derivedStateOf { lazyListState.firstVisibleItemScrollOffset } }
|
||||
|
||||
val walletItemSize by remember(lazyListState) {
|
||||
derivedStateOf { lazyListState.layoutInfo.visibleItemsInfo.firstOrNull()?.size ?: 1 }
|
||||
}
|
||||
|
||||
val walletHalfItemSize by remember {
|
||||
derivedStateOf { walletItemSize / 2 }
|
||||
}
|
||||
|
||||
val contentOffsetY by remember {
|
||||
derivedStateOf {
|
||||
/*
|
||||
* Until [walletItemOffset] is less than [walletHalfItemSize],
|
||||
* then content offset animation has positive value (downward movement).
|
||||
* Otherwise, it has negative value (upward movement).
|
||||
*/
|
||||
val position = if (walletItemOffset < walletHalfItemSize) {
|
||||
walletItemOffset
|
||||
} else {
|
||||
walletItemSize - walletItemOffset
|
||||
}
|
||||
|
||||
ANIMATION_MAX_OFFSET.dp / walletItemSize * position
|
||||
}
|
||||
}
|
||||
|
||||
val contentAlpha by remember {
|
||||
derivedStateOf {
|
||||
/*
|
||||
* Until [walletItemOffset] is less than [walletHalfItemSize],
|
||||
* then content alpha animation has negative value (fade out).
|
||||
* Otherwise, it has positive value (fade in).
|
||||
*/
|
||||
val position = if (walletItemOffset < walletHalfItemSize) {
|
||||
walletHalfItemSize - walletItemOffset
|
||||
} else {
|
||||
walletItemOffset - walletHalfItemSize
|
||||
}
|
||||
|
||||
ANIMATION_MAX_ALPHA / walletHalfItemSize * position
|
||||
}
|
||||
}
|
||||
|
||||
val animatedOffsetY by animateDpAsState(targetValue = contentOffsetY)
|
||||
val animatedContentAlpha by animateFloatAsState(targetValue = contentAlpha)
|
||||
|
||||
this
|
||||
.absoluteOffset(y = animatedOffsetY)
|
||||
.alpha(alpha = animatedContentAlpha)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.viewmodels
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
|
|
@ -32,5 +33,25 @@ internal class WalletViewModel @Inject constructor() : ViewModel() {
|
|||
topBarConfig = WalletPreviewData.multicurrencyWalletScreenState.topBarConfig.copy(
|
||||
onScanCardClick = { router.openOrganizeTokensScreen() },
|
||||
),
|
||||
walletsListConfig = WalletPreviewData.multicurrencyWalletScreenState.walletsListConfig.copy(
|
||||
onWalletChange = ::selectWallet,
|
||||
),
|
||||
)
|
||||
|
||||
// TODO: [REDACTED_TASK_KEY] Use production data instead of WalletPreviewData
|
||||
private fun selectWallet(index: Int) {
|
||||
if (uiState.walletsListConfig.selectedWalletIndex == index) return
|
||||
|
||||
Log.i("WalletViewModel", "selectWallet: $index")
|
||||
|
||||
uiState = if (index % 2 == 0) {
|
||||
WalletPreviewData.multicurrencyWalletScreenState.copy(
|
||||
walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index),
|
||||
)
|
||||
} else {
|
||||
WalletPreviewData.singleWalletScreenState.copy(
|
||||
walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue