From a13ace780e9a04aaa9f4e60f4cdba48177b9b7eb Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 May 2026 13:37:26 +0200 Subject: [PATCH] Updated on 2026-08-14 --- .../ui/components/tokenlist/TokenListItem.kt | 25 ++++--- .../core/ui/utils/SharedTransitionUtils.kt | 71 ++++++++++++++++--- .../multicurrency/MultiCurrencyContent.kt | 5 +- 3 files changed, 75 insertions(+), 26 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt index 7ae2238fb7..6b180bf5c1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt @@ -40,6 +40,7 @@ import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.ProvideSharedTransitionScope +import com.tangem.core.ui.utils.sharedBoundsSafely const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST" @@ -109,12 +110,11 @@ fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolea CurrencyIcon( state = iconState, withFixedSize = false, - modifier = modifier - .sharedBounds( - sharedContentState = iconSharedContentState, - animatedVisibilityScope = animatedContentScope, - boundsTransform = boundsTransform, - ), + modifier = modifier.sharedBoundsSafely( + sharedContentState = iconSharedContentState, + animatedVisibilityScope = animatedContentScope, + boundsTransform = boundsTransform, + ), ) }, title = { modifier: Modifier -> @@ -132,13 +132,12 @@ fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolea TokenTitle( state = state.tokenItemUM.titleState, textStyle = textStyle.copy(fontSize = textSize.sp), - modifier = modifier - .sharedBounds( - sharedContentState = titleSharedContentState, - animatedVisibilityScope = animatedContentScope, - boundsTransform = boundsTransform, - resizeMode = scaleToBounds(ContentScale.Fit, Alignment.CenterStart), - ), + modifier = modifier.sharedBoundsSafely( + sharedContentState = titleSharedContentState, + animatedVisibilityScope = animatedContentScope, + boundsTransform = boundsTransform, + resizeMode = scaleToBounds(ContentScale.Fit, Alignment.CenterStart), + ), ) }, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/SharedTransitionUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/SharedTransitionUtils.kt index 801c42088c..d9b6832de5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/SharedTransitionUtils.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/SharedTransitionUtils.kt @@ -1,24 +1,64 @@ package com.tangem.core.ui.utils -import androidx.compose.animation.AnimatedVisibilityScope -import androidx.compose.animation.BoundsTransform -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.SharedTransitionLayout -import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.* import androidx.compose.foundation.layout.Box -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Shape import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection +/** + * Default `true`: composables outside [ProvideSharedTransitionScope] keep previous behaviour. + * Inside [ProvideSharedTransitionScope], becomes `true` after the wrapper has received attached + * [LayoutCoordinates] from [Modifier.onGloballyPositioned]. + * + * Workaround for Compose Animation: shared bounds may detach before coordinates exist inside SubcomposeLayout + * slots (LazyColumn, Scaffold topBar, etc.). + * + * See [discussion](https://stackoverflow.com/questions/79466980/jetpack-compose-sharedbounds-inside-centeralignedtopappbar-crashes-on-first-scre). + */ +private val LocalSharedBoundsLayoutCoordinatesReady = compositionLocalOf { true } + +/** + * Crash-safe wrapper around [SharedTransitionScope.sharedBounds]: applies the modifier only after the enclosing + * [ProvideSharedTransitionScope] has reported attached layout coordinates; otherwise returns the receiver unchanged. + * + * Outside [ProvideSharedTransitionScope] the readiness flag defaults to `true`, and the scope falls back to a stub + * whose `sharedBounds` is a no-op, so the call is always safe. + */ +@Composable +fun Modifier.sharedBoundsSafely( + sharedContentState: SharedTransitionScope.SharedContentState, + animatedVisibilityScope: AnimatedVisibilityScope, + boundsTransform: BoundsTransform, + resizeMode: SharedTransitionScope.ResizeMode? = null, +): Modifier { + if (!LocalSharedBoundsLayoutCoordinatesReady.current) return this + val sharedTransitionScope = LocalSharedTransitionScope.current + return with(sharedTransitionScope) { + if (resizeMode != null) { + this@sharedBoundsSafely.sharedBounds( + sharedContentState = sharedContentState, + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = boundsTransform, + resizeMode = resizeMode, + ) + } else { + this@sharedBoundsSafely.sharedBounds( + sharedContentState = sharedContentState, + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = boundsTransform, + ) + } + } +} + @Composable fun TangemSharedTransitionLayout( modifier: Modifier = Modifier, @@ -37,8 +77,17 @@ fun TangemSharedTransitionLayout( @Composable fun ProvideSharedTransitionScope(modifier: Modifier = Modifier, content: @Composable SharedTransitionScope.() -> Unit) { val sharedTransitionScope = LocalSharedTransitionScope.current - Box(modifier) { - sharedTransitionScope.content() + var isLayoutCoordinatesReady by remember { mutableStateOf(false) } + Box( + modifier.onGloballyPositioned { coordinates -> + if (coordinates.isAttached) { + isLayoutCoordinatesReady = true + } + }, + ) { + CompositionLocalProvider(LocalSharedBoundsLayoutCoordinatesReady provides isLayoutCoordinatesReady) { + sharedTransitionScope.content() + } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index 42c872c101..169dd4b6d5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -56,6 +56,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.test.MainScreenTestTags import com.tangem.core.ui.utils.ProvideSharedTransitionScope import com.tangem.core.ui.utils.lazyListItemPosition +import com.tangem.core.ui.utils.sharedBoundsSafely import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.TokensListItemUM2 import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState @@ -389,7 +390,7 @@ internal fun PortfolioRowItem( tangemIconUM = sizedHeadIcon, modifier = modifier .size(iconBoxSize) - .sharedBounds( + .sharedBoundsSafely( sharedContentState = iconSharedContentState, animatedVisibilityScope = animatedContentScope, boundsTransform = boundsTransform, @@ -423,7 +424,7 @@ internal fun PortfolioRowItem( TokenRowTitle( titleUM = resizedTitle, - modifier = modifier.sharedBounds( + modifier = modifier.sharedBoundsSafely( sharedContentState = titleSharedContentState, animatedVisibilityScope = animatedContentScope, boundsTransform = boundsTransform,