fix: using unique key for transition

This commit is contained in:
tapframe 2026-04-11 20:00:41 +05:30
parent 81af639fe9
commit fa7e3aabfc
5 changed files with 38 additions and 15 deletions

View file

@ -171,6 +171,7 @@ data class PersonDetailRoute(
val personId: Int, val personId: Int,
val personName: String, val personName: String,
val personPhoto: String? = null, val personPhoto: String? = null,
val castAvatarTransitionKey: String? = null,
val preferCrew: Boolean = false, val preferCrew: Boolean = false,
) )
@ -766,7 +767,7 @@ private fun MainAppContent(
) )
} }
}, },
onCastClick = { person -> onCastClick = { person, avatarTransitionKey ->
val tmdbId = person.tmdbId val tmdbId = person.tmdbId
if (tmdbId != null && tmdbId > 0) { if (tmdbId != null && tmdbId > 0) {
navController.navigate( navController.navigate(
@ -774,6 +775,7 @@ private fun MainAppContent(
personId = tmdbId, personId = tmdbId,
personName = person.name, personName = person.name,
personPhoto = person.photo, personPhoto = person.photo,
castAvatarTransitionKey = avatarTransitionKey,
preferCrew = person.role?.let { preferCrew = person.role?.let {
it.equals("Director", ignoreCase = true) || it.equals("Director", ignoreCase = true) ||
it.equals("Writer", ignoreCase = true) || it.equals("Writer", ignoreCase = true) ||
@ -807,6 +809,7 @@ private fun MainAppContent(
personId = route.personId, personId = route.personId,
personName = route.personName, personName = route.personName,
initialProfilePhoto = route.personPhoto, initialProfilePhoto = route.personPhoto,
avatarTransitionKey = route.castAvatarTransitionKey,
preferCrew = route.preferCrew, preferCrew = route.preferCrew,
onBack = { navController.popBackStack() }, onBack = { navController.popBackStack() },
onOpenMeta = { preview -> onOpenMeta = { preview ->

View file

@ -1,3 +1,11 @@
package com.nuvio.app.features.details package com.nuvio.app.features.details
internal fun castAvatarSharedTransitionKey(personId: Int): String = "cast-avatar:$personId" internal fun castAvatarSharedTransitionKey(
personId: Int,
occurrenceIndex: Int? = null,
): String =
if (occurrenceIndex != null) {
"cast-avatar:$personId:$occurrenceIndex"
} else {
"cast-avatar:$personId"
}

View file

@ -107,7 +107,7 @@ fun MetaDetailsScreen(
onPlay: ((type: String, videoId: String, parentMetaId: String, parentMetaType: String, title: String, logo: String?, poster: String?, background: String?, seasonNumber: Int?, episodeNumber: Int?, episodeTitle: String?, episodeThumbnail: String?, pauseDescription: String?, resumePositionMs: Long?) -> Unit)? = null, onPlay: ((type: String, videoId: String, parentMetaId: String, parentMetaType: String, title: String, logo: String?, poster: String?, background: String?, seasonNumber: Int?, episodeNumber: Int?, episodeTitle: String?, episodeThumbnail: String?, pauseDescription: String?, resumePositionMs: Long?) -> Unit)? = null,
onPlayManually: ((type: String, videoId: String, parentMetaId: String, parentMetaType: String, title: String, logo: String?, poster: String?, background: String?, seasonNumber: Int?, episodeNumber: Int?, episodeTitle: String?, episodeThumbnail: String?, pauseDescription: String?, resumePositionMs: Long?) -> Unit)? = null, onPlayManually: ((type: String, videoId: String, parentMetaId: String, parentMetaType: String, title: String, logo: String?, poster: String?, background: String?, seasonNumber: Int?, episodeNumber: Int?, episodeTitle: String?, episodeThumbnail: String?, pauseDescription: String?, resumePositionMs: Long?) -> Unit)? = null,
onOpenMeta: ((MetaPreview) -> Unit)? = null, onOpenMeta: ((MetaPreview) -> Unit)? = null,
onCastClick: ((MetaPerson) -> Unit)? = null, onCastClick: ((MetaPerson, String?) -> Unit)? = null,
onCompanyClick: ((MetaCompany, String) -> Unit)? = null, onCompanyClick: ((MetaCompany, String) -> Unit)? = null,
sharedTransitionScope: SharedTransitionScope? = null, sharedTransitionScope: SharedTransitionScope? = null,
animatedVisibilityScope: AnimatedVisibilityScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null,
@ -919,7 +919,7 @@ private fun ConfiguredMetaSections(
onEpisodeClick: (MetaVideo) -> Unit, onEpisodeClick: (MetaVideo) -> Unit,
onEpisodeLongPress: (MetaVideo) -> Unit, onEpisodeLongPress: (MetaVideo) -> Unit,
onOpenMeta: ((MetaPreview) -> Unit)?, onOpenMeta: ((MetaPreview) -> Unit)?,
onCastClick: ((MetaPerson) -> Unit)?, onCastClick: ((MetaPerson, String?) -> Unit)?,
onCompanyClick: ((MetaCompany, String) -> Unit)?, onCompanyClick: ((MetaCompany, String) -> Unit)?,
sharedTransitionScope: SharedTransitionScope?, sharedTransitionScope: SharedTransitionScope?,
animatedVisibilityScope: AnimatedVisibilityScope?, animatedVisibilityScope: AnimatedVisibilityScope?,

View file

@ -73,6 +73,7 @@ fun PersonDetailScreen(
personId: Int, personId: Int,
personName: String, personName: String,
initialProfilePhoto: String? = null, initialProfilePhoto: String? = null,
avatarTransitionKey: String? = null,
preferCrew: Boolean = false, preferCrew: Boolean = false,
onBack: () -> Unit, onBack: () -> Unit,
onOpenMeta: (MetaPreview) -> Unit, onOpenMeta: (MetaPreview) -> Unit,
@ -81,6 +82,7 @@ fun PersonDetailScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
var uiState by remember(personId) { mutableStateOf<PersonDetailUiState>(PersonDetailUiState.Loading) } var uiState by remember(personId) { mutableStateOf<PersonDetailUiState>(PersonDetailUiState.Loading) }
val resolvedAvatarTransitionKey = avatarTransitionKey ?: castAvatarSharedTransitionKey(personId)
LaunchedEffect(personId) { LaunchedEffect(personId) {
uiState = PersonDetailUiState.Loading uiState = PersonDetailUiState.Loading
@ -105,6 +107,7 @@ fun PersonDetailScreen(
personId = personId, personId = personId,
personName = personName, personName = personName,
profilePhoto = initialProfilePhoto, profilePhoto = initialProfilePhoto,
avatarTransitionKey = resolvedAvatarTransitionKey,
sharedTransitionScope = sharedTransitionScope, sharedTransitionScope = sharedTransitionScope,
animatedVisibilityScope = animatedVisibilityScope, animatedVisibilityScope = animatedVisibilityScope,
) )
@ -119,6 +122,7 @@ fun PersonDetailScreen(
person = state.personDetail, person = state.personDetail,
onOpenMeta = onOpenMeta, onOpenMeta = onOpenMeta,
initialProfilePhoto = initialProfilePhoto, initialProfilePhoto = initialProfilePhoto,
avatarTransitionKey = resolvedAvatarTransitionKey,
sharedTransitionScope = sharedTransitionScope, sharedTransitionScope = sharedTransitionScope,
animatedVisibilityScope = animatedVisibilityScope, animatedVisibilityScope = animatedVisibilityScope,
) )
@ -147,6 +151,7 @@ private fun PersonDetailContent(
person: PersonDetail, person: PersonDetail,
onOpenMeta: (MetaPreview) -> Unit, onOpenMeta: (MetaPreview) -> Unit,
initialProfilePhoto: String? = null, initialProfilePhoto: String? = null,
avatarTransitionKey: String,
sharedTransitionScope: SharedTransitionScope? = null, sharedTransitionScope: SharedTransitionScope? = null,
animatedVisibilityScope: AnimatedVisibilityScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null,
) { ) {
@ -240,6 +245,7 @@ private fun PersonDetailContent(
person = person, person = person,
collapseProgress = collapseProgress, collapseProgress = collapseProgress,
fallbackProfilePhoto = initialProfilePhoto, fallbackProfilePhoto = initialProfilePhoto,
avatarTransitionKey = avatarTransitionKey,
sharedTransitionScope = sharedTransitionScope, sharedTransitionScope = sharedTransitionScope,
animatedVisibilityScope = animatedVisibilityScope, animatedVisibilityScope = animatedVisibilityScope,
) )
@ -292,6 +298,7 @@ private fun HeroSection(
person: PersonDetail, person: PersonDetail,
collapseProgress: Float = 0f, collapseProgress: Float = 0f,
fallbackProfilePhoto: String? = null, fallbackProfilePhoto: String? = null,
avatarTransitionKey: String,
sharedTransitionScope: SharedTransitionScope? = null, sharedTransitionScope: SharedTransitionScope? = null,
animatedVisibilityScope: AnimatedVisibilityScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null,
) { ) {
@ -299,7 +306,7 @@ private fun HeroSection(
val heroScale = 1f - (collapseProgress * 0.12f) val heroScale = 1f - (collapseProgress * 0.12f)
val heroAlpha = 1f - (collapseProgress * 0.35f) val heroAlpha = 1f - (collapseProgress * 0.35f)
val avatarUrl = person.profilePhoto?.takeIf { it.isNotBlank() } ?: fallbackProfilePhoto val avatarUrl = person.profilePhoto?.takeIf { it.isNotBlank() } ?: fallbackProfilePhoto
val avatarCacheKey = castAvatarSharedTransitionKey(person.tmdbId) val avatarCacheKey = avatarTransitionKey
val platformContext = LocalPlatformContext.current val platformContext = LocalPlatformContext.current
val avatarRequest = if (!avatarUrl.isNullOrBlank()) { val avatarRequest = if (!avatarUrl.isNullOrBlank()) {
remember(platformContext, avatarUrl, avatarCacheKey) { remember(platformContext, avatarUrl, avatarCacheKey) {
@ -317,7 +324,7 @@ private fun HeroSection(
with(sharedTransitionScope) { with(sharedTransitionScope) {
Modifier.sharedElement( Modifier.sharedElement(
sharedContentState = rememberSharedContentState( sharedContentState = rememberSharedContentState(
key = castAvatarSharedTransitionKey(person.tmdbId), key = avatarTransitionKey,
), ),
animatedVisibilityScope = animatedVisibilityScope, animatedVisibilityScope = animatedVisibilityScope,
) )
@ -434,11 +441,12 @@ private fun PersonDetailSkeleton(
personId: Int, personId: Int,
personName: String, personName: String,
profilePhoto: String? = null, profilePhoto: String? = null,
avatarTransitionKey: String,
sharedTransitionScope: SharedTransitionScope? = null, sharedTransitionScope: SharedTransitionScope? = null,
animatedVisibilityScope: AnimatedVisibilityScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null,
) { ) {
val accentColor = MaterialTheme.colorScheme.primary val accentColor = MaterialTheme.colorScheme.primary
val avatarCacheKey = castAvatarSharedTransitionKey(personId) val avatarCacheKey = avatarTransitionKey
val platformContext = LocalPlatformContext.current val platformContext = LocalPlatformContext.current
val avatarRequest = if (!profilePhoto.isNullOrBlank()) { val avatarRequest = if (!profilePhoto.isNullOrBlank()) {
remember(platformContext, profilePhoto, avatarCacheKey) { remember(platformContext, profilePhoto, avatarCacheKey) {
@ -492,7 +500,7 @@ private fun PersonDetailSkeleton(
with(sharedTransitionScope) { with(sharedTransitionScope) {
Modifier.sharedElement( Modifier.sharedElement(
sharedContentState = rememberSharedContentState( sharedContentState = rememberSharedContentState(
key = castAvatarSharedTransitionKey(personId), key = avatarTransitionKey,
), ),
animatedVisibilityScope = animatedVisibilityScope, animatedVisibilityScope = animatedVisibilityScope,
) )

View file

@ -41,7 +41,7 @@ fun DetailCastSection(
cast: List<MetaPerson>, cast: List<MetaPerson>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showHeader: Boolean = true, showHeader: Boolean = true,
onCastClick: ((MetaPerson) -> Unit)? = null, onCastClick: ((MetaPerson, String?) -> Unit)? = null,
sharedTransitionScope: SharedTransitionScope? = null, sharedTransitionScope: SharedTransitionScope? = null,
animatedVisibilityScope: AnimatedVisibilityScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null,
) { ) {
@ -61,14 +61,18 @@ fun DetailCastSection(
itemsIndexed( itemsIndexed(
items = cast, items = cast,
key = { index, person -> "${person.name}-${person.role.orEmpty()}-${person.photo.orEmpty()}-$index" }, key = { index, person -> "${person.name}-${person.role.orEmpty()}-${person.photo.orEmpty()}-$index" },
) { _, person -> ) { index, person ->
val sharedTransitionKey = person.tmdbId
?.takeIf { it > 0 }
?.let { castAvatarSharedTransitionKey(it, occurrenceIndex = index) }
CastItem( CastItem(
person = person, person = person,
sharedTransitionKey = sharedTransitionKey,
sizing = sizing, sizing = sizing,
sharedTransitionScope = sharedTransitionScope, sharedTransitionScope = sharedTransitionScope,
animatedVisibilityScope = animatedVisibilityScope, animatedVisibilityScope = animatedVisibilityScope,
onClick = if (onCastClick != null && person.tmdbId != null && person.tmdbId > 0) { onClick = if (onCastClick != null && person.tmdbId != null && person.tmdbId > 0) {
{ onCastClick(person) } { onCastClick(person, sharedTransitionKey) }
} else { } else {
null null
}, },
@ -84,12 +88,13 @@ fun DetailCastSection(
private fun CastItem( private fun CastItem(
person: MetaPerson, person: MetaPerson,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
sharedTransitionKey: String? = null,
sizing: CastSectionSizing, sizing: CastSectionSizing,
sharedTransitionScope: SharedTransitionScope? = null, sharedTransitionScope: SharedTransitionScope? = null,
animatedVisibilityScope: AnimatedVisibilityScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
) { ) {
val avatarCacheKey = person.tmdbId?.takeIf { it > 0 }?.let(::castAvatarSharedTransitionKey) val avatarCacheKey = sharedTransitionKey
val platformContext = LocalPlatformContext.current val platformContext = LocalPlatformContext.current
val avatarRequest = if (!person.photo.isNullOrBlank() && !avatarCacheKey.isNullOrBlank()) { val avatarRequest = if (!person.photo.isNullOrBlank() && !avatarCacheKey.isNullOrBlank()) {
remember(platformContext, person.photo, avatarCacheKey) { remember(platformContext, person.photo, avatarCacheKey) {
@ -107,13 +112,12 @@ private fun CastItem(
val avatarSharedElementModifier = if ( val avatarSharedElementModifier = if (
sharedTransitionScope != null && sharedTransitionScope != null &&
animatedVisibilityScope != null && animatedVisibilityScope != null &&
person.tmdbId != null && !sharedTransitionKey.isNullOrBlank()
person.tmdbId > 0
) { ) {
with(sharedTransitionScope) { with(sharedTransitionScope) {
Modifier.sharedElement( Modifier.sharedElement(
sharedContentState = rememberSharedContentState( sharedContentState = rememberSharedContentState(
key = castAvatarSharedTransitionKey(person.tmdbId), key = sharedTransitionKey,
), ),
animatedVisibilityScope = animatedVisibilityScope, animatedVisibilityScope = animatedVisibilityScope,
) )