Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-18 13:37:26 +02:00
parent 3e9f54f2db
commit a13ace780e
3 changed files with 75 additions and 26 deletions

View file

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

View file

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