diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt index 027bad2d0c..068172b71e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt @@ -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, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 5bc16b167a..9d4f2cf480 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -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( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 883b5c626d..fb462feaa9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -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, ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletManageButtons.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletManageButtons.kt index 1fd2c49db9..7d19a06e5a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletManageButtons.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletManageButtons.kt @@ -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, 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) }, + ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 2246159c2f..bf417cd9b1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -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, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index bba70f8c1a..6c39b86f10 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -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(), + ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt new file mode 100644 index 0000000000..b0b2072867 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt @@ -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> { + + private val LazyListItemInfo.halfItemSize get() = size.div(other = 2) + + override suspend fun emit(value: List) { + 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) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletContentItemAnimator.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletContentItemAnimator.kt new file mode 100644 index 0000000000..61564d220c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletContentItemAnimator.kt @@ -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) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index dd643d7352..e13dad1715 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -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), + ) + } + } } \ No newline at end of file