Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-02 16:05:28 +08:00
parent 9112d2bab8
commit 890048f5b2
9 changed files with 194 additions and 57 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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