Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-05 11:47:30 +03:00
parent 12fc61bbdc
commit 9334609daf
19 changed files with 297 additions and 206 deletions

View file

@ -43,6 +43,7 @@
android:resizeableActivity="@bool/resizeable_activity"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:enableOnBackInvokedCallback="true"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:allowBackup, android:fullBackupContent, android:label">

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.details.ui.common
import androidx.activity.compose.BackHandler
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.selection.selectable
@ -29,8 +28,6 @@ internal fun SettingsScreensScaffold(
) {
val backgroundColor = TangemTheme.colors.background.secondary
BackHandler(onBack = onBackClick)
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {

View file

@ -2,14 +2,18 @@ package com.tangem.tap.features.home
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.arkivanov.essenty.lifecycle.subscribe
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.SystemBarsIconsDisposable
import com.tangem.core.ui.res.LocalRootBackgroundColor
import com.tangem.core.ui.utils.findActivity
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.home.api.HomeComponent
@ -61,6 +65,17 @@ internal class DefaultHomeComponent @AssistedInject constructor(
onShopButtonClick = model::onShopClick,
onSearchTokensClick = model::onSearchClick,
)
val rootBackgroundColor = LocalRootBackgroundColor.current
val previousColor = remember { rootBackgroundColor.value }
val storiesBackgroundColor = Color(color = 0xFF010101)
DisposableEffect(Unit) {
rootBackgroundColor.value = storiesBackgroundColor
onDispose {
rootBackgroundColor.value = previousColor
}
}
}
override fun newState(state: HomeState) {

View file

@ -1,6 +1,7 @@
package com.tangem.tap.routing
import android.app.Activity
import android.os.Build
import android.os.Bundle
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@ -8,7 +9,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -17,25 +17,31 @@ import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat.startActivity
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
import com.arkivanov.decompose.extensions.compose.stack.animation.StackAnimation
import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.androidPredictiveBackAnimatable
import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.predictiveBackAnimation
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.value.Value
import com.arkivanov.essenty.backhandler.BackHandler
import com.tangem.common.routing.AppRoute
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
import com.tangem.core.ui.message.EventMessageEffect
import com.tangem.core.ui.res.LocalIsNavigationRefactoringEnabled
import com.tangem.core.ui.res.LocalRootBackgroundColor
import com.tangem.core.ui.res.LocalSnackbarHostState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.routing.component.RoutingComponent
import com.tangem.tap.routing.transitions.RoutingTransitionAnimationFactory
@OptIn(ExperimentalDecomposeApi::class)
@Composable
internal fun RootContent(
stack: Value<ChildStack<AppRoute, RoutingComponent.Child>>,
wcContent: @Composable (modifier: Modifier) -> Unit,
backHandler: BackHandler,
uiDependencies: UiDependencies,
wcContent: @Composable (modifier: Modifier) -> Unit,
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
@ -44,45 +50,62 @@ internal fun RootContent(
activity = context as Activity,
uiDependencies = uiDependencies,
) {
CompositionLocalProvider(
LocalIsNavigationRefactoringEnabled provides true,
) {
val snackbarHostState = LocalSnackbarHostState.current
val snackbarHostState = LocalSnackbarHostState.current
Box(Modifier.background(TangemTheme.colors.background.primary)) {
Children(
modifier = modifier,
animation = stackAnimation(fade()),
stack = stack,
) { child ->
when (val instance = child.instance) {
is RoutingComponent.Child.Initial -> Unit
is RoutingComponent.Child.ComposableComponent -> {
instance.component.Content(Modifier.fillMaxSize())
Box(Modifier.background(LocalRootBackgroundColor.current.value)) {
Children(
modifier = modifier,
animation = childrenAnimation(backHandler = backHandler, onBack = onBack),
stack = stack,
) { child ->
when (val instance = child.instance) {
is RoutingComponent.Child.Initial -> Unit
is RoutingComponent.Child.ComposableComponent -> {
instance.component.Content(Modifier.fillMaxSize())
}
is RoutingComponent.Child.LegacyIntent -> {
// TODO: Remove and use it's own router: [REDACTED_JIRA]
LaunchedEffect(instance) {
startActivity(context, instance.intent, Bundle.EMPTY)
}
is RoutingComponent.Child.LegacyIntent -> {
// TODO: Remove and use it's own router: [REDACTED_JIRA]
LaunchedEffect(instance) {
startActivity(context, instance.intent, Bundle.EMPTY)
}
}
is RoutingComponent.Child.LegacyFragment,
-> error("Unsupported child: $instance")
}
}
wcContent(Modifier.fillMaxSize())
TangemSnackbarHost(
modifier = Modifier
.align(Alignment.BottomCenter)
.navigationBarsPadding()
.padding(all = 16.dp),
hostState = snackbarHostState,
)
}
}
EventMessageEffect()
wcContent(Modifier.fillMaxSize())
TangemSnackbarHost(
modifier = Modifier
.align(Alignment.BottomCenter)
.navigationBarsPadding()
.padding(all = 16.dp),
hostState = snackbarHostState,
)
}
}
EventMessageEffect()
}
@OptIn(ExperimentalDecomposeApi::class)
private fun childrenAnimation(
backHandler: BackHandler,
onBack: () -> Unit,
): StackAnimation<AppRoute, RoutingComponent.Child> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
predictiveBackAnimation(
backHandler = backHandler,
onBack = onBack,
selector = { backEvent, _, _ ->
androidPredictiveBackAnimatable(backEvent)
},
fallbackAnimation = stackAnimation {
RoutingTransitionAnimationFactory.create(it.configuration)
},
)
} else {
stackAnimation {
RoutingTransitionAnimationFactory.create(it.configuration)
}
}
}

View file

@ -2,11 +2,9 @@ package com.tangem.tap.routing.component
import android.content.Intent
import androidx.compose.runtime.Immutable
import androidx.fragment.app.Fragment
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.utils.Provider
internal interface RoutingComponent : ComposableContentComponent {
@ -16,11 +14,6 @@ internal interface RoutingComponent : ComposableContentComponent {
// TODO: Remove and find initial intent in RoutingComponent: [REDACTED_JIRA]
data object Initial : Child()
data class LegacyFragment(
val name: String,
val fragmentProvider: Provider<Fragment>,
) : Child()
data class LegacyIntent(val intent: Intent) : Child()
data class ComposableComponent(

View file

@ -74,6 +74,8 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
stack = stack,
uiDependencies = uiDependencies,
wcContent = { wcRoutingComponent.Content(it) },
backHandler = backHandler,
onBack = router::pop,
)
}

View file

@ -0,0 +1,92 @@
package com.tangem.tap.routing.transitions
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layout
import com.arkivanov.decompose.extensions.compose.stack.animation.*
import com.tangem.common.routing.AppRoute
import kotlin.compareTo
import kotlin.times
object RoutingTransitionAnimationFactory {
@Suppress("MagicNumber")
fun create(appRoute: AppRoute): StackAnimator {
return when (appRoute) {
is AppRoute.Onboarding,
is AppRoute.Welcome,
is AppRoute.Home,
-> fade(tween(400)).plus(scale(tween(400)))
is AppRoute.Wallet,
-> slideAndFade(directions = setOf(Direction.ENTER_BACK, Direction.EXIT_BACK))
.plus(
scaleWithDirection(
directions = setOf(Direction.ENTER_FRONT, Direction.EXIT_FRONT),
animationSpec = tween(400),
),
)
else -> slideAndFade()
}
}
private fun scaleWithDirection(
directions: Set<Direction>,
animationSpec: FiniteAnimationSpec<Float> = tween(),
frontFactor: Float = 1.15F,
backFactor: Float = 0.95F,
): StackAnimator = stackAnimator(animationSpec = animationSpec) { factor, direction, content ->
content(
Modifier.graphicsLayer {
val scaleFactor = if (directions.contains(direction)) {
if (factor >= 0F) {
factor * (frontFactor - 1F) + 1F
} else {
factor * (1F - backFactor) + 1F
}
} else {
1f
}
scaleX = scaleFactor
scaleY = scaleFactor
},
)
}
@Suppress("MagicNumber")
private fun slideAndFade(directions: Set<Direction>? = null): StackAnimator {
val easing = CubicBezierEasing(0.55f, 0.0f, 0.0f, 1f)
return stackAnimator(
animationSpec = tween(durationMillis = 400, easing = easing),
) { factor, direction, content ->
content(
if (directions == null || directions.contains(direction)) {
Modifier.offsetXFactor(factor)
} else {
Modifier
},
)
}.plus(
fade(
animationSpec = tween(
delayMillis = 50,
durationMillis = 300,
easing = easing,
),
),
)
}
@Suppress("MagicNumber")
private fun Modifier.offsetXFactor(factor: Float): Modifier = layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.placeRelative(x = (placeable.width.toFloat() * factor * 0.15f).toInt(), y = 0)
}
}
}