Updated on 2026-08-14
This commit is contained in:
commit
dd1ae618bb
312 changed files with 5079 additions and 2207 deletions
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.tangem.feature.wallet.impl">
|
||||
</manifest>
|
||||
|
|
@ -87,11 +87,13 @@ private fun PriceChangeIcon(type: PriceChangeType) {
|
|||
id = when (animatedType) {
|
||||
PriceChangeType.UP -> R.drawable.ic_arrow_up_8
|
||||
PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8
|
||||
PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8
|
||||
},
|
||||
),
|
||||
tint = when (animatedType) {
|
||||
PriceChangeType.UP -> TangemTheme.colors.icon.accent
|
||||
PriceChangeType.DOWN -> TangemTheme.colors.icon.warning
|
||||
PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
|
|
@ -106,6 +108,7 @@ private fun PriceChangeText(type: PriceChangeType?, text: String?, modifier: Mod
|
|||
color = when (type) {
|
||||
PriceChangeType.UP -> TangemTheme.colors.text.accent
|
||||
PriceChangeType.DOWN -> TangemTheme.colors.text.warning
|
||||
PriceChangeType.NEUTRAL -> TangemTheme.colors.text.disabled
|
||||
null -> TangemTheme.colors.text.tertiary
|
||||
},
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScree
|
|||
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel
|
||||
import com.tangem.features.managetokens.navigation.ExpandableState
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensUi
|
||||
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
|
||||
import kotlin.properties.Delegates
|
||||
|
|
@ -62,10 +63,13 @@ internal class DefaultWalletRouter(
|
|||
state = viewModel.uiState.collectAsStateWithLifecycle().value,
|
||||
bottomSheetHeaderHeightProvider = { bottomSheetHeaderHeight },
|
||||
bottomSheetContent = {
|
||||
val state = remember { mutableStateOf(ExpandableState.COLLAPSED) }
|
||||
// Manage Tokens
|
||||
manageTokensUi.Content(
|
||||
onHeaderSizeChange = { bottomSheetHeaderHeight = it },
|
||||
state = state,
|
||||
)
|
||||
viewModel.setExpandableState(state)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,5 +58,6 @@ internal interface InnerWalletRouter : WalletRouter {
|
|||
/** Open manage tokens screen */
|
||||
fun openManageTokensScreen()
|
||||
|
||||
/** Open scan failed dialog */
|
||||
fun openScanFailedDialog()
|
||||
}
|
||||
|
|
@ -14,22 +14,23 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
|||
import com.tangem.domain.tokens.model.NetworkGroup
|
||||
import com.tangem.domain.tokens.model.TokenList
|
||||
import com.tangem.domain.tokens.repository.PromoRepository
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.count
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ViewModelScoped
|
||||
internal class GetMultiWalletWarningsFactory @Inject constructor(
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val getTokenListUseCase: GetTokenListUseCase,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
||||
|
|
@ -40,15 +41,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
|
||||
private var readyForRateAppNotification = false
|
||||
|
||||
fun create(clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
|
||||
val userWallet = getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = {
|
||||
Timber.e("Failed to get selected wallet $it")
|
||||
return flowOf(value = persistentListOf())
|
||||
},
|
||||
ifRight = { it },
|
||||
)
|
||||
|
||||
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
|
||||
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
||||
|
||||
val promoFlow = flow { emit(promoRepository.getChangellyPromoBanner()) }
|
||||
|
|
|
|||
|
|
@ -9,21 +9,17 @@ import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
|
|||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ViewModelScoped
|
||||
internal class GetSingleWalletWarningsFactory @Inject constructor(
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
||||
|
|
@ -33,15 +29,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor(
|
|||
|
||||
private var readyForRateAppNotification = false
|
||||
|
||||
fun create(clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
|
||||
val userWallet = getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = {
|
||||
Timber.e("Failed to get selected wallet $it")
|
||||
return flowOf(value = persistentListOf())
|
||||
},
|
||||
ifRight = { it },
|
||||
)
|
||||
|
||||
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
|
||||
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
||||
|
||||
return combine(
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ internal class MultiWalletContentLoader(
|
|||
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
|
||||
),
|
||||
MultiWalletWarningsSubscriber(
|
||||
userWalletId = userWallet.walletId,
|
||||
userWallet = userWallet,
|
||||
stateHolder = stateHolder,
|
||||
clickIntents = clickIntents,
|
||||
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ internal class SingleWalletContentLoader(
|
|||
getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase,
|
||||
),
|
||||
SingleWalletNotificationsSubscriber(
|
||||
userWalletId = userWallet.walletId,
|
||||
userWallet = userWallet,
|
||||
stateHolder = stateHolder,
|
||||
clickIntents = clickIntents,
|
||||
getSingleWalletWarningsFactory = getSingleWalletWarningsFactory,
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ internal class SingleWalletWithTokenContentLoader(
|
|||
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
|
||||
),
|
||||
MultiWalletWarningsSubscriber(
|
||||
userWalletId = userWallet.walletId,
|
||||
userWallet = userWallet,
|
||||
stateHolder = stateHolder,
|
||||
clickIntents = clickIntents,
|
||||
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
|
@ -11,6 +12,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBot
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.WalletScreenStateTransformer
|
||||
import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles
|
||||
import com.tangem.features.managetokens.navigation.ExpandableState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -86,6 +88,7 @@ internal class WalletStateController @Inject constructor(
|
|||
event = consumedEvent(),
|
||||
isHidingMode = false,
|
||||
manageTokenRedesignToggle = manageTokensFeatureToggles.isRedesignedScreenEnabled,
|
||||
manageTokensExpandableState = mutableStateOf(ExpandableState.EXPANDED),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -27,12 +27,12 @@ sealed class WalletNotification(val config: NotificationConfig) {
|
|||
),
|
||||
) {
|
||||
|
||||
object DevCard : Critical(
|
||||
data object DevCard : Critical(
|
||||
title = resourceReference(id = R.string.warning_developer_card_title),
|
||||
subtitle = resourceReference(id = R.string.warning_developer_card_message),
|
||||
)
|
||||
|
||||
object FailedCardValidation : Critical(
|
||||
data object FailedCardValidation : Critical(
|
||||
title = resourceReference(id = R.string.warning_failed_to_verify_card_title),
|
||||
subtitle = resourceReference(id = R.string.warning_failed_to_verify_card_message),
|
||||
)
|
||||
|
|
@ -64,12 +64,12 @@ sealed class WalletNotification(val config: NotificationConfig) {
|
|||
),
|
||||
)
|
||||
|
||||
object NetworksUnreachable : Warning(
|
||||
data object NetworksUnreachable : Warning(
|
||||
title = resourceReference(id = R.string.warning_network_unreachable_title),
|
||||
subtitle = resourceReference(id = R.string.warning_network_unreachable_message),
|
||||
)
|
||||
|
||||
object SomeNetworksUnreachable : Warning(
|
||||
data object SomeNetworksUnreachable : Warning(
|
||||
title = resourceReference(id = R.string.warning_some_networks_unreachable_title),
|
||||
subtitle = resourceReference(id = R.string.warning_some_networks_unreachable_message),
|
||||
)
|
||||
|
|
@ -80,7 +80,7 @@ sealed class WalletNotification(val config: NotificationConfig) {
|
|||
onCloseClick = onCloseClick,
|
||||
)
|
||||
|
||||
object TestNetCard : Warning(
|
||||
data object TestNetCard : Warning(
|
||||
title = resourceReference(id = R.string.warning_testnet_card_title),
|
||||
subtitle = resourceReference(id = R.string.warning_testnet_card_message),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.model
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.features.managetokens.navigation.ExpandableState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class WalletScreenState(
|
||||
val onBackClick: () -> Unit,
|
||||
val manageTokensExpandableState: MutableState<ExpandableState>,
|
||||
val topBarConfig: WalletTopBarConfig,
|
||||
val selectedWalletIndex: Int,
|
||||
val wallets: ImmutableList<WalletState>,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert
|
|||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class SingleWalletMarketPriceConverter(
|
||||
private val status: CryptoCurrencyStatus.Status,
|
||||
|
|
@ -61,8 +61,6 @@ internal class SingleWalletMarketPriceConverter(
|
|||
}
|
||||
|
||||
private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeType {
|
||||
val priceChange = status.priceChange ?: return PriceChangeType.DOWN
|
||||
|
||||
return if (priceChange > BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN
|
||||
return PriceChangeConverter.fromBigDecimal(status.priceChange)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert
|
|||
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
|
|
@ -97,8 +98,6 @@ internal class TokenItemStateConverter(
|
|||
priceChangePercent = BigDecimalFormatter.formatPercent(
|
||||
percent = priceChange,
|
||||
useAbsoluteValue = true,
|
||||
maxFractionDigits = 1,
|
||||
minFractionDigits = 1,
|
||||
),
|
||||
type = priceChange.getPriceChangeType(),
|
||||
)
|
||||
|
|
@ -117,6 +116,6 @@ internal class TokenItemStateConverter(
|
|||
}
|
||||
|
||||
private fun BigDecimal.getPriceChangeType(): PriceChangeType {
|
||||
return if (this > BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN
|
||||
return PriceChangeConverter.fromBigDecimal(value = this)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.subscribers
|
||||
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||
|
|
@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
internal class MultiWalletWarningsSubscriber(
|
||||
private val userWalletId: UserWalletId,
|
||||
private val userWallet: UserWallet,
|
||||
private val stateHolder: WalletStateController,
|
||||
private val clickIntents: WalletClickIntents,
|
||||
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
|
||||
|
|
@ -23,13 +23,13 @@ internal class MultiWalletWarningsSubscriber(
|
|||
) : WalletSubscriber() {
|
||||
|
||||
override fun create(coroutineScope: CoroutineScope): Flow<ImmutableList<WalletNotification>> {
|
||||
return getMultiWalletWarningsFactory.create(clickIntents)
|
||||
return getMultiWalletWarningsFactory.create(userWallet, clickIntents)
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { warnings ->
|
||||
val displayedState = stateHolder.getWalletState(userWalletId)
|
||||
val displayedState = stateHolder.getWalletState(userWallet.walletId)
|
||||
|
||||
stateHolder.update(SetWarningsTransformer(userWalletId, warnings))
|
||||
stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings))
|
||||
walletWarningsAnalyticsSender.send(displayedState, warnings)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.subscribers
|
||||
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||
|
|
@ -18,7 +18,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class SingleWalletNotificationsSubscriber(
|
||||
private val userWalletId: UserWalletId,
|
||||
private val userWallet: UserWallet,
|
||||
private val stateHolder: WalletStateController,
|
||||
private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory,
|
||||
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
|
||||
|
|
@ -26,13 +26,13 @@ internal class SingleWalletNotificationsSubscriber(
|
|||
) : WalletSubscriber() {
|
||||
|
||||
override fun create(coroutineScope: CoroutineScope): Flow<ImmutableList<WalletNotification>> {
|
||||
return getSingleWalletWarningsFactory.create(clickIntents)
|
||||
return getSingleWalletWarningsFactory.create(userWallet, clickIntents)
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { warnings ->
|
||||
val displayedState = stateHolder.getWalletState(userWalletId)
|
||||
val displayedState = stateHolder.getWalletState(userWallet.walletId)
|
||||
|
||||
stateHolder.update(SetWarningsTransformer(userWalletId, warnings))
|
||||
stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings))
|
||||
walletWarningsAnalyticsSender.send(displayedState, warnings)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.core.TweenSpec
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
|
|
@ -13,16 +17,19 @@ import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
|||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import com.tangem.core.ui.components.Keyboard
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
import com.tangem.core.ui.components.atoms.Hand
|
||||
import com.tangem.core.ui.components.atoms.handComposableComponentHeight
|
||||
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet
|
||||
|
|
@ -46,6 +53,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.VisaTxDe
|
|||
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balancesAndLimitsBlock
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.depositButton
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
|
||||
import com.tangem.features.managetokens.navigation.ExpandableState
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -64,6 +72,13 @@ internal fun WalletScreen(
|
|||
val snackbarHostState = remember(::SnackbarHostState)
|
||||
val isAutoScroll = remember { mutableStateOf(value = false) }
|
||||
|
||||
var alertConfig by remember { mutableStateOf<WalletAlertState?>(value = null) }
|
||||
|
||||
val config = alertConfig
|
||||
if (config != null) {
|
||||
WalletAlert(state = config, onDismiss = { alertConfig = null })
|
||||
}
|
||||
|
||||
WalletContent(
|
||||
state = state,
|
||||
walletsListState = walletsListState,
|
||||
|
|
@ -72,14 +87,9 @@ internal fun WalletScreen(
|
|||
onAutoScrollReset = { isAutoScroll.value = false },
|
||||
bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider,
|
||||
bottomSheetContent = bottomSheetContent,
|
||||
alertConfig = alertConfig,
|
||||
)
|
||||
|
||||
var alertConfig by remember { mutableStateOf<WalletAlertState?>(value = null) }
|
||||
|
||||
alertConfig?.let {
|
||||
WalletAlert(state = it, onDismiss = { alertConfig = null })
|
||||
}
|
||||
|
||||
WalletEventEffect(
|
||||
event = state.event,
|
||||
selectedWalletIndex = state.selectedWalletIndex,
|
||||
|
|
@ -100,8 +110,9 @@ private fun WalletContent(
|
|||
bottomSheetHeaderHeightProvider: () -> Dp,
|
||||
onAutoScrollReset: () -> Unit,
|
||||
bottomSheetContent: @Composable () -> Unit,
|
||||
alertConfig: WalletAlertState?,
|
||||
) {
|
||||
var selectedWalletIndex by remember { mutableIntStateOf(state.selectedWalletIndex) }
|
||||
var selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) }
|
||||
val selectedWallet = state.wallets[selectedWalletIndex]
|
||||
|
||||
val scaffoldContent: @Composable () -> Unit = {
|
||||
|
|
@ -207,6 +218,7 @@ private fun WalletContent(
|
|||
snackbarHostState = snackbarHostState,
|
||||
bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider,
|
||||
bottomSheetContent = bottomSheetContent,
|
||||
alertConfig = alertConfig,
|
||||
) {
|
||||
scaffoldContent()
|
||||
}
|
||||
|
|
@ -222,7 +234,7 @@ private fun WalletContent(
|
|||
}
|
||||
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun BaseScaffoldManageTokenRedesign(
|
||||
state: WalletScreenState,
|
||||
|
|
@ -230,49 +242,47 @@ private fun BaseScaffoldManageTokenRedesign(
|
|||
snackbarHostState: SnackbarHostState,
|
||||
bottomSheetHeaderHeightProvider: () -> Dp,
|
||||
bottomSheetContent: @Composable () -> Unit,
|
||||
alertConfig: WalletAlertState?,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val scaffoldState = rememberBottomSheetScaffoldState()
|
||||
// show the bottom sheet if there is at least one multicurrency wallet
|
||||
val showManageTokensBottomSheet = remember(state.wallets) {
|
||||
state.wallets.any { it is WalletState.MultiCurrency }
|
||||
}
|
||||
val bottomSheetState = rememberSheetStateEnhanced(
|
||||
initialValue = if (showManageTokensBottomSheet) SheetValue.PartiallyExpanded else SheetValue.Hidden,
|
||||
confirmValueChange = { sheetValue ->
|
||||
when {
|
||||
sheetValue == SheetValue.Hidden && showManageTokensBottomSheet -> false
|
||||
sheetValue != SheetValue.Hidden && !showManageTokensBottomSheet -> false
|
||||
else -> true
|
||||
}
|
||||
},
|
||||
skipHiddenState = showManageTokensBottomSheet,
|
||||
)
|
||||
|
||||
val keyboardShown = keyboardAsState()
|
||||
|
||||
BottomSheetStateEffects(
|
||||
bottomSheetState = bottomSheetState,
|
||||
state = state,
|
||||
showManageTokensBottomSheet = showManageTokensBottomSheet,
|
||||
alertConfig = alertConfig,
|
||||
keyboardShown = keyboardShown,
|
||||
)
|
||||
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = bottomSheetState,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val navigationBarColor = TangemTheme.colors.background.primary
|
||||
val navigationBarColorWithout = TangemTheme.colors.background.secondary
|
||||
|
||||
DisposableEffect(
|
||||
navigationBarColor,
|
||||
navigationBarColorWithout,
|
||||
) {
|
||||
systemUiController.setNavigationBarColor(navigationBarColor)
|
||||
onDispose {
|
||||
systemUiController.setNavigationBarColor(navigationBarColorWithout)
|
||||
}
|
||||
}
|
||||
|
||||
val keyboardShown by keyboardAsState()
|
||||
// expand bottom sheet when keyboard appears
|
||||
LaunchedEffect(keyboardShown is Keyboard.Opened) {
|
||||
if (keyboardShown is Keyboard.Opened) {
|
||||
scaffoldState.bottomSheetState.expand()
|
||||
}
|
||||
}
|
||||
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val sheetHasBeenHidden = scaffoldState.bottomSheetState.targetValue == SheetValue.PartiallyExpanded
|
||||
// hide keyboard when bottom sheet is about to be hidden
|
||||
LaunchedEffect(sheetHasBeenHidden) {
|
||||
if (sheetHasBeenHidden) {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
}
|
||||
|
||||
val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
BottomSheetScaffold(
|
||||
topBar = {
|
||||
WalletTopBar(config = state.topBarConfig)
|
||||
},
|
||||
snackbarHost = {
|
||||
SnackbarHost(hostState = snackbarHostState)
|
||||
},
|
||||
|
|
@ -296,10 +306,10 @@ private fun BaseScaffoldManageTokenRedesign(
|
|||
|
||||
// hide bottom sheet when back pressed
|
||||
BackHandler(
|
||||
keyboardShown is Keyboard.Closed &&
|
||||
scaffoldState.bottomSheetState.currentValue == SheetValue.Expanded,
|
||||
keyboardShown.value is Keyboard.Closed &&
|
||||
bottomSheetState.currentValue == SheetValue.Expanded,
|
||||
) {
|
||||
coroutineScope.launch { scaffoldState.bottomSheetState.partialExpand() }
|
||||
coroutineScope.launch { bottomSheetState.partialExpand() }
|
||||
}
|
||||
},
|
||||
content = { paddingValues ->
|
||||
|
|
@ -308,23 +318,164 @@ private fun BaseScaffoldManageTokenRedesign(
|
|||
onRefresh = selectedWallet.pullToRefreshConfig.onRefresh,
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.pullRefresh(pullRefreshState)
|
||||
.padding(paddingValues),
|
||||
Column(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
) {
|
||||
content()
|
||||
WalletTopBar(config = state.topBarConfig)
|
||||
Box(
|
||||
modifier = Modifier.pullRefresh(pullRefreshState),
|
||||
) {
|
||||
content()
|
||||
|
||||
WalletPullToRefreshIndicator(
|
||||
isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
|
||||
state = pullRefreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
)
|
||||
WalletPullToRefreshIndicator(
|
||||
isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
|
||||
state = pullRefreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BottomSheetScrim(
|
||||
color = BottomSheetDefaults.ScrimColor,
|
||||
visible = bottomSheetState.targetValue == SheetValue.Expanded,
|
||||
onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: () -> Unit) {
|
||||
val alpha by animateFloatAsState(
|
||||
targetValue = if (visible) 1f else 0f,
|
||||
animationSpec = TweenSpec(),
|
||||
label = "scrim",
|
||||
)
|
||||
val dismissSheet = if (visible) {
|
||||
Modifier
|
||||
.pointerInput(onDismissRequest) {
|
||||
detectTapGestures {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
.clearAndSetSemantics {}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Canvas(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.then(dismissSheet),
|
||||
) {
|
||||
drawRect(color = color, alpha = alpha)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
@Composable
|
||||
private fun BottomSheetStateEffects(
|
||||
bottomSheetState: SheetState,
|
||||
state: WalletScreenState,
|
||||
showManageTokensBottomSheet: Boolean,
|
||||
alertConfig: WalletAlertState?,
|
||||
keyboardShown: State<Keyboard>,
|
||||
) {
|
||||
// Bottom sheet during initialization internally expand partially after its content was remeasured,
|
||||
// therefore initialValue = SheetValue.Hidden in rememberStandardBottomSheetState doesn't work as expected
|
||||
// so we have to manually restrict expansion in this case
|
||||
LaunchedEffect(bottomSheetState.targetValue, bottomSheetState.currentValue) {
|
||||
if (!showManageTokensBottomSheet &&
|
||||
(bottomSheetState.targetValue != SheetValue.Hidden || bottomSheetState.currentValue != SheetValue.Hidden)
|
||||
) {
|
||||
bottomSheetState.hide()
|
||||
}
|
||||
}
|
||||
// react to changes in wallet list
|
||||
LaunchedEffect(showManageTokensBottomSheet) {
|
||||
when {
|
||||
showManageTokensBottomSheet && bottomSheetState.currentValue != SheetValue.PartiallyExpanded -> {
|
||||
bottomSheetState.partialExpand()
|
||||
}
|
||||
!showManageTokensBottomSheet && bottomSheetState.targetValue != SheetValue.Hidden -> {
|
||||
bottomSheetState.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val navigationBarColor = TangemTheme.colors.background.primary
|
||||
val navigationBarColorWithout = TangemTheme.colors.background.secondary
|
||||
|
||||
SystemBarsEffect {
|
||||
if (showManageTokensBottomSheet) {
|
||||
setNavigationBarColor(navigationBarColor)
|
||||
}
|
||||
}
|
||||
DisposableEffect(
|
||||
showManageTokensBottomSheet,
|
||||
) {
|
||||
onDispose {
|
||||
if (showManageTokensBottomSheet) {
|
||||
systemUiController.setNavigationBarColor(navigationBarColorWithout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// expand bottom sheet when keyboard appears
|
||||
LaunchedEffect(keyboardShown.value is Keyboard.Opened) {
|
||||
if (keyboardShown.value is Keyboard.Opened && alertConfig == null) {
|
||||
bottomSheetState.expand()
|
||||
}
|
||||
}
|
||||
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
// hide keyboard when bottom sheet is about to be hidden
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow {
|
||||
bottomSheetState.currentValue == SheetValue.Expanded &&
|
||||
bottomSheetState.targetValue == SheetValue.PartiallyExpanded
|
||||
}.collect { sheetHasBeenHidden ->
|
||||
if (sheetHasBeenHidden) {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isSheetHidden = bottomSheetState.targetValue == SheetValue.PartiallyExpanded
|
||||
LaunchedEffect(isSheetHidden) {
|
||||
if (isSheetHidden) {
|
||||
state.manageTokensExpandableState.value = ExpandableState.COLLAPSED
|
||||
} else {
|
||||
state.manageTokensExpandableState.value = ExpandableState.EXPANDED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a standard method when this is fixed https://issuetracker.google.com/issues/314796718
|
||||
* Current material3 version: 1.2.0
|
||||
*/
|
||||
@Composable
|
||||
@ExperimentalMaterial3Api
|
||||
private fun rememberSheetStateEnhanced(
|
||||
skipPartiallyExpanded: Boolean = false,
|
||||
confirmValueChange: (SheetValue) -> Boolean = { true },
|
||||
initialValue: SheetValue = SheetValue.Hidden,
|
||||
skipHiddenState: Boolean = false,
|
||||
): SheetState {
|
||||
val density = LocalDensity.current
|
||||
return remember(initialValue, skipPartiallyExpanded, confirmValueChange, skipHiddenState) {
|
||||
SheetState(
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
density = density,
|
||||
initialValue = initialValue,
|
||||
confirmValueChange = confirmValueChange,
|
||||
skipHiddenState = skipHiddenState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun BaseScaffold(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.LazyListItemData
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.WalletsListInteractionsCollector
|
||||
|
||||
|
|
@ -19,7 +20,11 @@ internal fun WalletsListEffects(
|
|||
onAutoScrollReset: () -> Unit,
|
||||
) {
|
||||
LaunchedEffect(key1 = lazyListState, key2 = onWalletChange) {
|
||||
snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo }
|
||||
snapshotFlow {
|
||||
lazyListState.layoutInfo.visibleItemsInfo.map {
|
||||
LazyListItemData(it.index, it.size, it.offset)
|
||||
}
|
||||
}
|
||||
.collect(
|
||||
collector = ScrollOffsetCollector(
|
||||
selectedWalletIndex = selectedWalletIndex,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,405 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui.components
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.gestures.FlingBehavior
|
||||
import androidx.compose.foundation.gestures.ScrollScope
|
||||
import androidx.compose.ui.MotionDurationScale
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.sign
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
class TangemSnapFlingBehavior(
|
||||
private val snapLayoutInfoProvider: SnapLayoutInfoProvider,
|
||||
private val lowVelocityAnimationSpec: AnimationSpec<Float>,
|
||||
private val highVelocityAnimationSpec: DecayAnimationSpec<Float>,
|
||||
private val snapAnimationSpec: AnimationSpec<Float>,
|
||||
private val density: Density,
|
||||
private val shortSnapVelocityThreshold: Dp = MinFlingVelocityDp,
|
||||
) : FlingBehavior {
|
||||
|
||||
private val velocityThreshold = with(density) { shortSnapVelocityThreshold.toPx() }
|
||||
private var motionScaleDuration = DefaultScrollMotionDurationScale
|
||||
|
||||
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
|
||||
return performFling(initialVelocity) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a snapping fling animation with given velocity and suspend until fling has
|
||||
* finished. This will behave the same way as [performFling] except it will report on
|
||||
* each remainingOffsetUpdate using the [onSettlingDistanceUpdated] lambda.
|
||||
*
|
||||
* @param initialVelocity velocity available for fling in the orientation specified in
|
||||
* [androidx.compose.foundation.gestures.scrollable] that invoked this method.
|
||||
*
|
||||
* @param onSettlingDistanceUpdated a lambda that will be called anytime the
|
||||
* distance to the settling offset is updated. The settling offset is the final offset where
|
||||
* this fling will stop and may change depending on the snapping animation progression.
|
||||
*
|
||||
* @return remaining velocity after fling operation has ended
|
||||
*/
|
||||
private suspend fun ScrollScope.performFling(
|
||||
initialVelocity: Float,
|
||||
onSettlingDistanceUpdated: (Float) -> Unit,
|
||||
): Float {
|
||||
val (remainingOffset, remainingState) = fling(initialVelocity, onSettlingDistanceUpdated)
|
||||
|
||||
// No remaining offset means we've used everything, no need to propagate velocity. Otherwise
|
||||
// we couldn't use everything (probably because we have hit the min/max bounds of the
|
||||
// containing layout) we should propagate the offset.
|
||||
return if (remainingOffset == 0f) NoVelocity else remainingState.velocity
|
||||
}
|
||||
|
||||
private suspend fun ScrollScope.fling(
|
||||
initialVelocity: Float,
|
||||
onRemainingScrollOffsetUpdate: (Float) -> Unit,
|
||||
): AnimationResult<Float, AnimationVector1D> {
|
||||
// If snapping from scroll (short snap) or fling (long snap)
|
||||
val result = withContext(motionScaleDuration) {
|
||||
if (abs(initialVelocity) <= abs(velocityThreshold)) {
|
||||
shortSnap(initialVelocity, onRemainingScrollOffsetUpdate)
|
||||
} else {
|
||||
longSnap(initialVelocity, onRemainingScrollOffsetUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
onRemainingScrollOffsetUpdate(0f) // Animation finished or was cancelled
|
||||
return result
|
||||
}
|
||||
|
||||
private suspend fun ScrollScope.shortSnap(
|
||||
velocity: Float,
|
||||
onRemainingScrollOffsetUpdate: (Float) -> Unit,
|
||||
): AnimationResult<Float, AnimationVector1D> {
|
||||
val closestOffset = with(snapLayoutInfoProvider) {
|
||||
density.calculateSnappingOffset(0f)
|
||||
}
|
||||
|
||||
var remainingScrollOffset = closestOffset
|
||||
|
||||
val animationState = AnimationState(NoDistance, velocity)
|
||||
return animateSnap(
|
||||
closestOffset,
|
||||
closestOffset,
|
||||
animationState,
|
||||
snapAnimationSpec,
|
||||
) { delta ->
|
||||
remainingScrollOffset -= delta
|
||||
onRemainingScrollOffsetUpdate(remainingScrollOffset)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ScrollScope.longSnap(
|
||||
initialVelocity: Float,
|
||||
onAnimationStep: (remainingScrollOffset: Float) -> Unit,
|
||||
): AnimationResult<Float, AnimationVector1D> {
|
||||
val initialOffset =
|
||||
with(snapLayoutInfoProvider) { density.calculateApproachOffset(initialVelocity) }.let {
|
||||
abs(it) * sign(initialVelocity) // ensure offset sign is correct
|
||||
}
|
||||
var remainingScrollOffset = initialOffset
|
||||
|
||||
onAnimationStep(remainingScrollOffset) // First Scroll Offset
|
||||
|
||||
val (remainingOffset, animationState) = runApproach(
|
||||
initialOffset,
|
||||
initialVelocity,
|
||||
) { delta ->
|
||||
remainingScrollOffset -= delta
|
||||
onAnimationStep(remainingScrollOffset)
|
||||
}
|
||||
|
||||
remainingScrollOffset = remainingOffset
|
||||
|
||||
return animateSnap(
|
||||
remainingOffset,
|
||||
remainingOffset,
|
||||
animationState.copy(value = 0f),
|
||||
snapAnimationSpec,
|
||||
) { delta ->
|
||||
remainingScrollOffset -= delta
|
||||
onAnimationStep(remainingScrollOffset)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ScrollScope.runApproach(
|
||||
initialTargetOffset: Float,
|
||||
initialVelocity: Float,
|
||||
onAnimationStep: (delta: Float) -> Unit,
|
||||
): AnimationResult<Float, AnimationVector1D> {
|
||||
val animation =
|
||||
if (isDecayApproachPossible(offset = initialTargetOffset, velocity = initialVelocity)) {
|
||||
HighVelocityApproachAnimation(highVelocityAnimationSpec)
|
||||
} else {
|
||||
LowVelocityApproachAnimation(
|
||||
lowVelocityAnimationSpec,
|
||||
snapLayoutInfoProvider,
|
||||
density,
|
||||
)
|
||||
}
|
||||
|
||||
return approach(
|
||||
initialTargetOffset,
|
||||
initialVelocity,
|
||||
animation,
|
||||
snapLayoutInfoProvider,
|
||||
density,
|
||||
onAnimationStep,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* If we can approach the target and still have velocity left
|
||||
*/
|
||||
private fun isDecayApproachPossible(offset: Float, velocity: Float): Boolean {
|
||||
val decayOffset = highVelocityAnimationSpec.calculateTargetValue(NoDistance, velocity)
|
||||
val snapStepSize = with(snapLayoutInfoProvider) { density.calculateSnapStepSize() }
|
||||
return decayOffset.absoluteValue >= offset.absoluteValue + snapStepSize
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other is TangemSnapFlingBehavior) {
|
||||
other.snapAnimationSpec == this.snapAnimationSpec &&
|
||||
other.highVelocityAnimationSpec == this.highVelocityAnimationSpec &&
|
||||
other.lowVelocityAnimationSpec == this.lowVelocityAnimationSpec &&
|
||||
other.snapLayoutInfoProvider == this.snapLayoutInfoProvider &&
|
||||
other.density == this.density &&
|
||||
other.shortSnapVelocityThreshold == this.shortSnapVelocityThreshold
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = 0
|
||||
.let { 31 * it + snapAnimationSpec.hashCode() }
|
||||
.let { 31 * it + highVelocityAnimationSpec.hashCode() }
|
||||
.let { 31 * it + lowVelocityAnimationSpec.hashCode() }
|
||||
.let { 31 * it + snapLayoutInfoProvider.hashCode() }
|
||||
.let { 31 * it + density.hashCode() }
|
||||
.let { 31 * it + shortSnapVelocityThreshold.hashCode() }
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private suspend fun ScrollScope.approach(
|
||||
initialTargetOffset: Float,
|
||||
initialVelocity: Float,
|
||||
animation: ApproachAnimation<Float, AnimationVector1D>,
|
||||
snapLayoutInfoProvider: SnapLayoutInfoProvider,
|
||||
density: Density,
|
||||
onAnimationStep: (delta: Float) -> Unit,
|
||||
): AnimationResult<Float, AnimationVector1D> {
|
||||
val (_, currentAnimationState) = animation.approachAnimation(
|
||||
this,
|
||||
initialTargetOffset,
|
||||
initialVelocity,
|
||||
onAnimationStep,
|
||||
)
|
||||
|
||||
val remainingOffset = with(snapLayoutInfoProvider) {
|
||||
density.calculateSnappingOffset(currentAnimationState.velocity)
|
||||
}
|
||||
|
||||
// will snap the remainder
|
||||
return AnimationResult(remainingOffset, currentAnimationState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a [AnimationSpec] to snap the list into [targetOffset]. Uses [cancelOffset] to stop this
|
||||
* animation before it reaches the target.
|
||||
*
|
||||
* @param targetOffset The final target of this animation
|
||||
* @param cancelOffset If we'd like to finish the animation earlier we use this value
|
||||
* @param animationState The current animation state for continuation purposes
|
||||
* @param snapAnimationSpec The [AnimationSpec] that will drive this animation
|
||||
* @param onAnimationStep Called for each new scroll delta emitted by the animation cycle.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
private suspend fun ScrollScope.animateSnap(
|
||||
targetOffset: Float,
|
||||
cancelOffset: Float,
|
||||
animationState: AnimationState<Float, AnimationVector1D>,
|
||||
snapAnimationSpec: AnimationSpec<Float>,
|
||||
onAnimationStep: (delta: Float) -> Unit,
|
||||
): AnimationResult<Float, AnimationVector1D> {
|
||||
var consumedUpToNow = 0f
|
||||
val initialVelocity = animationState.velocity
|
||||
animationState.animateTo(
|
||||
targetOffset,
|
||||
animationSpec = snapAnimationSpec,
|
||||
sequentialAnimation = animationState.velocity != 0f,
|
||||
) {
|
||||
val realValue = value.coerceToTarget(cancelOffset)
|
||||
val delta = realValue - consumedUpToNow
|
||||
val consumed = scrollBy(delta)
|
||||
onAnimationStep(consumed)
|
||||
// stop when unconsumed or when we reach the desired value
|
||||
if (abs(delta - consumed) > 0.5f || realValue != value) {
|
||||
cancelAnimation()
|
||||
}
|
||||
consumedUpToNow += consumed
|
||||
}
|
||||
|
||||
// Always course correct velocity so they don't become too large.
|
||||
val finalVelocity = animationState.velocity.coerceToTarget(initialVelocity)
|
||||
return AnimationResult(
|
||||
targetOffset - consumedUpToNow,
|
||||
animationState.copy(velocity = finalVelocity),
|
||||
)
|
||||
}
|
||||
|
||||
private class HighVelocityApproachAnimation(
|
||||
private val decayAnimationSpec: DecayAnimationSpec<Float>,
|
||||
) : ApproachAnimation<Float, AnimationVector1D> {
|
||||
override suspend fun approachAnimation(
|
||||
scope: ScrollScope,
|
||||
offset: Float,
|
||||
velocity: Float,
|
||||
onAnimationStep: (delta: Float) -> Unit,
|
||||
): AnimationResult<Float, AnimationVector1D> {
|
||||
val animationState = AnimationState(initialValue = 0f, initialVelocity = velocity)
|
||||
return with(scope) {
|
||||
animateDecay(offset, animationState, decayAnimationSpec, onAnimationStep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class LowVelocityApproachAnimation @OptIn(ExperimentalFoundationApi::class) constructor(
|
||||
private val lowVelocityAnimationSpec: AnimationSpec<Float>,
|
||||
private val layoutInfoProvider: SnapLayoutInfoProvider,
|
||||
private val density: Density,
|
||||
) : ApproachAnimation<Float, AnimationVector1D> {
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
override suspend fun approachAnimation(
|
||||
scope: ScrollScope,
|
||||
offset: Float,
|
||||
velocity: Float,
|
||||
onAnimationStep: (delta: Float) -> Unit,
|
||||
): AnimationResult<Float, AnimationVector1D> {
|
||||
val animationState = AnimationState(initialValue = 0f, initialVelocity = velocity)
|
||||
val targetOffset =
|
||||
(abs(offset) + with(layoutInfoProvider) { density.calculateSnapStepSize() }) * sign(
|
||||
velocity,
|
||||
)
|
||||
return with(scope) {
|
||||
animateSnap(
|
||||
targetOffset = targetOffset,
|
||||
cancelOffset = offset,
|
||||
animationState = animationState,
|
||||
snapAnimationSpec = lowVelocityAnimationSpec,
|
||||
onAnimationStep = onAnimationStep,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private suspend fun ScrollScope.animateDecay(
|
||||
targetOffset: Float,
|
||||
animationState: AnimationState<Float, AnimationVector1D>,
|
||||
decayAnimationSpec: DecayAnimationSpec<Float>,
|
||||
onAnimationStep: (delta: Float) -> Unit,
|
||||
): AnimationResult<Float, AnimationVector1D> {
|
||||
var previousValue = 0f
|
||||
|
||||
fun AnimationScope<Float, AnimationVector1D>.consumeDelta(delta: Float) {
|
||||
val consumed = scrollBy(delta)
|
||||
onAnimationStep(consumed)
|
||||
if (abs(delta - consumed) > 0.5f) cancelAnimation()
|
||||
}
|
||||
|
||||
animationState.animateDecay(
|
||||
animationSpec = decayAnimationSpec,
|
||||
sequentialAnimation = animationState.velocity != 0f,
|
||||
) {
|
||||
previousValue = if (abs(value) >= abs(targetOffset)) {
|
||||
val finalValue = value.coerceToTarget(targetOffset)
|
||||
val finalDelta = finalValue - previousValue
|
||||
consumeDelta(finalDelta)
|
||||
cancelAnimation()
|
||||
finalValue
|
||||
} else {
|
||||
val delta = value - previousValue
|
||||
consumeDelta(delta)
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
return AnimationResult(
|
||||
targetOffset - previousValue,
|
||||
animationState,
|
||||
)
|
||||
}
|
||||
|
||||
private interface ApproachAnimation<T, V : AnimationVector> {
|
||||
suspend fun approachAnimation(
|
||||
scope: ScrollScope,
|
||||
offset: T,
|
||||
velocity: T,
|
||||
onAnimationStep: (delta: T) -> Unit,
|
||||
): AnimationResult<T, V>
|
||||
}
|
||||
|
||||
private fun Float.coerceToTarget(target: Float): Float {
|
||||
if (target == 0f) return 0f
|
||||
return if (target > 0) coerceAtMost(target) else coerceAtLeast(target)
|
||||
}
|
||||
|
||||
private class AnimationResult<T, V : AnimationVector>(
|
||||
val remainingOffset: T,
|
||||
val currentAnimationState: AnimationState<T, V>,
|
||||
) {
|
||||
operator fun component1(): T = remainingOffset
|
||||
operator fun component2(): AnimationState<T, V> = currentAnimationState
|
||||
}
|
||||
|
||||
@Suppress("TopLevelPropertyNaming")
|
||||
private const val DefaultScrollMotionDurationScaleFactor = 1f
|
||||
|
||||
@Suppress("TopLevelPropertyNaming")
|
||||
val DefaultScrollMotionDurationScale = object : MotionDurationScale {
|
||||
override val scaleFactor: Float
|
||||
get() = DefaultScrollMotionDurationScaleFactor
|
||||
}
|
||||
|
||||
@Suppress("TopLevelPropertyNaming")
|
||||
internal val MinFlingVelocityDp = 400.dp
|
||||
|
||||
@Suppress("TopLevelPropertyNaming")
|
||||
internal const val NoDistance = 0f
|
||||
|
||||
@Suppress("TopLevelPropertyNaming")
|
||||
internal const val NoVelocity = 0f
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
interface SnapLayoutInfoProvider {
|
||||
/**
|
||||
* The minimum offset that snapping will use to animate.(e.g. an item size)
|
||||
*/
|
||||
fun Density.calculateSnapStepSize(): Float
|
||||
|
||||
/**
|
||||
* Calculate the distance to navigate before settling into the next snapping bound.
|
||||
*
|
||||
* @param initialVelocity The current fling movement velocity. You can use this tho calculate a
|
||||
* velocity based offset.
|
||||
*/
|
||||
fun Density.calculateApproachOffset(initialVelocity: Float): Float
|
||||
|
||||
/**
|
||||
* Given a target placement in a layout, the snapping offset is the next snapping position
|
||||
* this layout can be placed in. If this is a short snapping, [currentVelocity] is guaranteed
|
||||
* to be 0.If it is a long snapping, this method will be called
|
||||
* after [calculateApproachOffset].
|
||||
*
|
||||
* @param currentVelocity The current fling movement velocity. This may change throughout the
|
||||
* fling animation.
|
||||
*/
|
||||
fun Density.calculateSnappingOffset(currentVelocity: Float): Float
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui.components
|
||||
|
||||
import androidx.compose.animation.core.DecayAnimationSpec
|
||||
import androidx.compose.animation.core.calculateTargetValue
|
||||
import androidx.compose.animation.splineBasedDecay
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
|
||||
import androidx.compose.foundation.lazy.LazyListLayoutInfo
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.ui.unit.Density
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.sign
|
||||
|
||||
/**
|
||||
* A [SnapLayoutInfoProvider] for LazyLists.
|
||||
*
|
||||
* @param lazyListState The [LazyListState] with information about the current state of the list
|
||||
* @param positionInLayout The desired positioning of the snapped item within the main layout.
|
||||
* This position should be considered with regard to the start edge of the item and the placement
|
||||
* within the viewport.
|
||||
*
|
||||
* @return A [SnapLayoutInfoProvider] that can be used with [SnapFlingBehavior]
|
||||
*/
|
||||
@Suppress("FunctionNaming")
|
||||
@ExperimentalFoundationApi
|
||||
fun TangemSnapLayoutInfoProvider(
|
||||
lazyListState: LazyListState,
|
||||
positionInLayout: SnapPositionInLayout = SnapPositionInLayout.CenterToCenter,
|
||||
): SnapLayoutInfoProvider = object : SnapLayoutInfoProvider {
|
||||
|
||||
private val layoutInfo: LazyListLayoutInfo
|
||||
get() = lazyListState.layoutInfo
|
||||
|
||||
// Decayed page snapping is the default
|
||||
override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
|
||||
val decayAnimationSpec: DecayAnimationSpec<Float> = splineBasedDecay(this)
|
||||
val offset =
|
||||
decayAnimationSpec.calculateTargetValue(NoDistance, initialVelocity).absoluteValue
|
||||
val finalDecayOffset = (offset - calculateSnapStepSize()).coerceAtLeast(0f)
|
||||
return if (finalDecayOffset == 0f) {
|
||||
finalDecayOffset
|
||||
} else {
|
||||
finalDecayOffset * initialVelocity.sign
|
||||
}
|
||||
}
|
||||
|
||||
override fun Density.calculateSnappingOffset(currentVelocity: Float): Float {
|
||||
var lowerBoundOffset = Float.NEGATIVE_INFINITY
|
||||
var upperBoundOffset = Float.POSITIVE_INFINITY
|
||||
|
||||
layoutInfo.visibleItemsInfo.fastForEach { item ->
|
||||
val offset =
|
||||
calculateDistanceToDesiredSnapPosition(
|
||||
mainAxisViewPortSize = layoutInfo.singleAxisViewportSize,
|
||||
beforeContentPadding = layoutInfo.beforeContentPadding,
|
||||
afterContentPadding = layoutInfo.afterContentPadding,
|
||||
itemSize = item.size,
|
||||
itemOffset = item.offset,
|
||||
itemIndex = item.index,
|
||||
snapPositionInLayout = positionInLayout,
|
||||
)
|
||||
|
||||
// Find item that is closest to the center
|
||||
if (offset <= 0 && offset > lowerBoundOffset) {
|
||||
lowerBoundOffset = offset
|
||||
}
|
||||
|
||||
// Find item that is closest to center, but after it
|
||||
if (offset >= 0 && offset < upperBoundOffset) {
|
||||
upperBoundOffset = offset
|
||||
}
|
||||
}
|
||||
|
||||
return calculateFinalOffset(currentVelocity, lowerBoundOffset, upperBoundOffset)
|
||||
}
|
||||
|
||||
override fun Density.calculateSnapStepSize(): Float = with(layoutInfo) {
|
||||
if (visibleItemsInfo.isNotEmpty()) {
|
||||
visibleItemsInfo.fastSumBy { it.size } / visibleItemsInfo.size.toFloat()
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("BanInlineOptIn")
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T> List<T>.fastSumBy(selector: (T) -> Int): Int {
|
||||
contract { callsInPlace(selector) }
|
||||
var sum = 0
|
||||
fastForEach { element ->
|
||||
sum += selector(element)
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
internal fun calculateFinalOffset(velocity: Float, lowerBound: Float, upperBound: Float): Float {
|
||||
fun Float.isValidDistance(): Boolean {
|
||||
return this != Float.POSITIVE_INFINITY && this != Float.NEGATIVE_INFINITY
|
||||
}
|
||||
|
||||
val finalDistance = when (sign(velocity)) {
|
||||
0f -> {
|
||||
if (abs(upperBound) <= abs(lowerBound)) {
|
||||
upperBound
|
||||
} else {
|
||||
lowerBound
|
||||
}
|
||||
}
|
||||
|
||||
1f -> upperBound
|
||||
-1f -> lowerBound
|
||||
else -> NoDistance
|
||||
}
|
||||
|
||||
return if (finalDistance.isValidDistance()) {
|
||||
finalDistance
|
||||
} else {
|
||||
NoDistance
|
||||
}
|
||||
}
|
||||
|
||||
internal val LazyListLayoutInfo.singleAxisViewportSize: Int
|
||||
get() = if (orientation == Orientation.Vertical) viewportSize.height else viewportSize.width
|
||||
|
||||
@Suppress("BanInlineOptIn")
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
|
||||
contract { callsInPlace(action) }
|
||||
for (index in indices) {
|
||||
val item = get(index)
|
||||
action(item)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal fun Density.calculateDistanceToDesiredSnapPosition(
|
||||
mainAxisViewPortSize: Int,
|
||||
beforeContentPadding: Int,
|
||||
afterContentPadding: Int,
|
||||
itemSize: Int,
|
||||
itemOffset: Int,
|
||||
itemIndex: Int,
|
||||
snapPositionInLayout: SnapPositionInLayout,
|
||||
): Float {
|
||||
val containerSize = mainAxisViewPortSize - beforeContentPadding - afterContentPadding
|
||||
|
||||
val desiredDistance = with(snapPositionInLayout) {
|
||||
position(containerSize, itemSize, itemIndex)
|
||||
}.toFloat()
|
||||
|
||||
return itemOffset - desiredDistance
|
||||
}
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
fun interface SnapPositionInLayout {
|
||||
/**
|
||||
* Calculates an offset positioning between a container and an element within this container.
|
||||
* The offset calculation is the necessary diff that should be applied to the item offset to
|
||||
* align the item with a position within the container. As a base line, if we wanted to align
|
||||
* the start of the container and the start of the item, we would return 0 in this function.
|
||||
*/
|
||||
fun Density.position(layoutSize: Int, itemSize: Int, itemIndex: Int): Int
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Aligns the center of the item with the center of the containing layout.
|
||||
*/
|
||||
val CenterToCenter =
|
||||
SnapPositionInLayout { layoutSize, itemSize, _ -> layoutSize / 2 - itemSize / 2 }
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,6 @@ import androidx.compose.animation.core.*
|
|||
import androidx.compose.animation.rememberSplineBasedDecay
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
|
||||
import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
|
||||
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
|
|
@ -31,7 +29,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.common.Wallet
|
|||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
private const val SHORT_SNAP_ELEMENT_COUNT = 50
|
||||
private const val SHORT_SNAP_ELEMENT_COUNT = 25
|
||||
|
||||
/**
|
||||
* Wallets list component
|
||||
|
|
@ -76,23 +74,20 @@ internal fun WalletsList(
|
|||
|
||||
/**
|
||||
* Custom implementation of fling behaviour that overrides 'shortSnapVelocityThreshold'.
|
||||
* Every user's drag action will similar to a short snap
|
||||
* if drag offset is less than [SHORT_SNAP_ELEMENT_COUNT] * item width.
|
||||
*
|
||||
* @param lazyListState lazy list state
|
||||
* @param itemWidth list item width
|
||||
*
|
||||
* @see rememberSnapFlingBehavior
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun rememberWalletsFlingBehaviour(lazyListState: LazyListState, itemWidth: Dp): SnapFlingBehavior {
|
||||
val snappingLayout = remember(lazyListState) { SnapLayoutInfoProvider(lazyListState) }
|
||||
private fun rememberWalletsFlingBehaviour(lazyListState: LazyListState, itemWidth: Dp): TangemSnapFlingBehavior {
|
||||
val snappingLayout = remember(lazyListState) { TangemSnapLayoutInfoProvider(lazyListState) }
|
||||
val density = LocalDensity.current
|
||||
val highVelocityApproachSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay()
|
||||
|
||||
return remember(key1 = snappingLayout, key2 = highVelocityApproachSpec, key3 = density) {
|
||||
SnapFlingBehavior(
|
||||
TangemSnapFlingBehavior(
|
||||
snapLayoutInfoProvider = snappingLayout,
|
||||
lowVelocityAnimationSpec = tween(durationMillis = 1000, easing = LinearEasing),
|
||||
highVelocityAnimationSpec = highVelocityApproachSpec,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui.utils
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlin.math.abs
|
||||
|
|
@ -20,14 +19,14 @@ internal class ScrollOffsetCollector(
|
|||
selectedWalletIndex: Int,
|
||||
private val lazyListState: LazyListState,
|
||||
private val onWalletChange: (Int) -> Unit,
|
||||
) : FlowCollector<List<LazyListItemInfo>> {
|
||||
) : FlowCollector<List<LazyListItemData>> {
|
||||
|
||||
private val LazyListItemInfo.halfItemSize
|
||||
private val LazyListItemData.halfItemSize
|
||||
get() = size.div(other = 2)
|
||||
|
||||
private var currentIndex = selectedWalletIndex
|
||||
|
||||
override suspend fun emit(value: List<LazyListItemInfo>) {
|
||||
override suspend fun emit(value: List<LazyListItemData>) {
|
||||
if (!lazyListState.isScrollInProgress || value.size <= 1) return
|
||||
|
||||
val firstItem = value.firstOrNull() ?: return
|
||||
|
|
@ -46,4 +45,10 @@ internal class ScrollOffsetCollector(
|
|||
onWalletChange(newIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal data class LazyListItemData(
|
||||
val index: Int,
|
||||
val size: Int,
|
||||
val offset: Int,
|
||||
)
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.viewmodels
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
|
@ -26,6 +27,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.*
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
|
||||
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
|
||||
import com.tangem.features.managetokens.navigation.ExpandableState
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
|
|
@ -81,6 +83,12 @@ internal class WalletViewModel @Inject constructor(
|
|||
clickIntents.initialize(router, viewModelScope)
|
||||
}
|
||||
|
||||
fun setExpandableState(state: MutableState<ExpandableState>) {
|
||||
stateHolder.update {
|
||||
it.copy(manageTokensExpandableState = state)
|
||||
}
|
||||
}
|
||||
|
||||
fun subscribeToLifecycle(lifecycleOwner: LifecycleOwner) {
|
||||
lifecycleOwner.lifecycle.addObserver(screenLifecycleProvider)
|
||||
}
|
||||
|
|
@ -170,7 +178,7 @@ internal class WalletViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateWallets(action: WalletsUpdateActionResolver.Action) {
|
||||
private suspend fun updateWallets(action: WalletsUpdateActionResolver.Action) {
|
||||
when (action) {
|
||||
is WalletsUpdateActionResolver.Action.InitializeWallets -> initializeWallets(action)
|
||||
is WalletsUpdateActionResolver.Action.ReinitializeWallets -> {
|
||||
|
|
@ -195,7 +203,7 @@ internal class WalletViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun initializeWallets(action: WalletsUpdateActionResolver.Action.InitializeWallets) {
|
||||
private suspend fun initializeWallets(action: WalletsUpdateActionResolver.Action.InitializeWallets) {
|
||||
walletScreenContentLoader.load(
|
||||
userWallet = action.selectedWallet,
|
||||
clickIntents = clickIntents,
|
||||
|
|
@ -211,100 +219,96 @@ internal class WalletViewModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
if (action.wallets.size > 1 && isWalletsScrollPreviewEnabled()) {
|
||||
withContext(dispatchers.io) {
|
||||
delay(timeMillis = 1_800)
|
||||
}
|
||||
if (action.wallets.size > 1 && isWalletsScrollPreviewEnabled()) {
|
||||
withContext(dispatchers.io) { delay(timeMillis = 1_800) }
|
||||
|
||||
walletEventSender.send(
|
||||
event = WalletEvent.DemonstrateWalletsScrollPreview(
|
||||
direction = if (action.selectedWalletIndex == action.wallets.lastIndex) {
|
||||
Direction.RIGHT
|
||||
} else {
|
||||
Direction.LEFT
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
walletEventSender.send(
|
||||
event = WalletEvent.DemonstrateWalletsScrollPreview(
|
||||
direction = if (action.selectedWalletIndex == action.wallets.lastIndex) {
|
||||
Direction.RIGHT
|
||||
} else {
|
||||
Direction.LEFT
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun reinitializeWallet(action: WalletsUpdateActionResolver.Action.ReinitializeWallet) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
walletScreenContentLoader.cancel(action.prevWalletId)
|
||||
walletScreenContentLoader.cancel(action.prevWalletId)
|
||||
|
||||
walletScreenContentLoader.load(
|
||||
userWallet = action.selectedWallet,
|
||||
clickIntents = clickIntents,
|
||||
coroutineScope = viewModelScope,
|
||||
)
|
||||
walletScreenContentLoader.load(
|
||||
userWallet = action.selectedWallet,
|
||||
clickIntents = clickIntents,
|
||||
coroutineScope = viewModelScope,
|
||||
)
|
||||
|
||||
stateHolder.update(
|
||||
ReinitializeWalletTransformer(userWallet = action.selectedWallet, clickIntents = clickIntents),
|
||||
)
|
||||
}
|
||||
stateHolder.update(
|
||||
ReinitializeWalletTransformer(userWallet = action.selectedWallet, clickIntents = clickIntents),
|
||||
)
|
||||
}
|
||||
|
||||
private fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
stateHolder.update(
|
||||
AddWalletTransformer(
|
||||
userWallet = action.selectedWallet,
|
||||
clickIntents = clickIntents,
|
||||
),
|
||||
)
|
||||
private suspend fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) {
|
||||
walletScreenContentLoader.load(
|
||||
userWallet = action.selectedWallet,
|
||||
clickIntents = clickIntents,
|
||||
coroutineScope = viewModelScope,
|
||||
)
|
||||
|
||||
walletScreenContentLoader.load(
|
||||
stateHolder.update(
|
||||
AddWalletTransformer(
|
||||
userWallet = action.selectedWallet,
|
||||
clickIntents = clickIntents,
|
||||
coroutineScope = viewModelScope,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
withContext(dispatchers.io) { delay(timeMillis = 700) }
|
||||
|
||||
scrollToWallet(index = action.selectedWalletIndex)
|
||||
}
|
||||
|
||||
private suspend fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) {
|
||||
walletScreenContentLoader.load(
|
||||
userWallet = action.selectedWallet,
|
||||
clickIntents = clickIntents,
|
||||
coroutineScope = viewModelScope,
|
||||
)
|
||||
|
||||
if (action.selectedWalletIndex != 0) {
|
||||
/*
|
||||
* If card is reset to factory settings, then Compose need some time to draw the WalletScreen.
|
||||
* Otherwise, scroll isn't happened
|
||||
*/
|
||||
withContext(dispatchers.io) { delay(timeMillis = 700) }
|
||||
|
||||
scrollToWallet(index = action.selectedWalletIndex)
|
||||
|
||||
withContext(dispatchers.io) { delay(timeMillis = 1000) }
|
||||
}
|
||||
|
||||
stateHolder.update(
|
||||
DeleteWalletTransformer(
|
||||
selectedWalletIndex = action.selectedWalletIndex,
|
||||
deletedWalletId = action.deletedWalletId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
walletScreenContentLoader.load(
|
||||
userWallet = action.selectedWallet,
|
||||
private suspend fun unlockWallet(action: WalletsUpdateActionResolver.Action.UnlockWallet) {
|
||||
withContext(dispatchers.io) { delay(timeMillis = 700) }
|
||||
|
||||
stateHolder.update(
|
||||
transformer = UnlockWalletTransformer(
|
||||
unlockedWallets = action.unlockedWallets,
|
||||
clickIntents = clickIntents,
|
||||
coroutineScope = viewModelScope,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
scrollToWallet(index = action.selectedWalletIndex)
|
||||
|
||||
withContext(dispatchers.io) { delay(timeMillis = 700) }
|
||||
|
||||
stateHolder.update(
|
||||
DeleteWalletTransformer(
|
||||
selectedWalletIndex = action.selectedWalletIndex,
|
||||
deletedWalletId = action.deletedWalletId,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun unlockWallet(action: WalletsUpdateActionResolver.Action.UnlockWallet) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
withContext(dispatchers.io) { delay(timeMillis = 700) }
|
||||
|
||||
stateHolder.update(
|
||||
transformer = UnlockWalletTransformer(
|
||||
unlockedWallets = action.unlockedWallets,
|
||||
clickIntents = clickIntents,
|
||||
),
|
||||
)
|
||||
|
||||
walletScreenContentLoader.load(
|
||||
userWallet = action.selectedWallet,
|
||||
clickIntents = clickIntents,
|
||||
coroutineScope = viewModelScope,
|
||||
)
|
||||
}
|
||||
walletScreenContentLoader.load(
|
||||
userWallet = action.selectedWallet,
|
||||
clickIntents = clickIntents,
|
||||
coroutineScope = viewModelScope,
|
||||
)
|
||||
}
|
||||
|
||||
private fun scrollToWallet(index: Int) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue