Search code examples
androidkotlinnavigationandroid-jetpack-compose

Shared element transition while using Destination library - Compose Android


Destination this library is life saver for maintaining screens in compose as it removed lots of boiler plate but now i want to use the shared element transition with it and i am struggling to build it as what should i pass in the parameter.

We can pass string, boolean, serialized class but what is required to be passed down to next screen for transition animation


Solution

  • You can pass Int, String, Double, or Float or any class as long as rememberSharedContentState(key: Any) keys match in sharedElement or sharedBounds in both screens.

    But using unique keys are suggested by google

    When working with complex shared elements, it is a good practice to create a key that is not a string, because strings can be error prone to match. Each key must be unique for matches to occur. For example, in Jetsnack we have the following shared elements:

    data class SnackSharedElementKey(
        val snackId: Long,
        val origin: String,
        val type: SnackSharedElementType
    )
    
    enum class SnackSharedElementType {
        Bounds,
        Image,
        Title,
        Tagline,
        Background
    }
    
    
    @Composable
    fun SharedElementUniqueKey() {
        // ...
                Box(
                    modifier = Modifier
                        .sharedElement(
                            rememberSharedContentState(
                                key = SnackSharedElementKey(
                                    snackId = 1,
                                    origin = "latest",
                                    type = SnackSharedElementType.Image
                                )
                            ),
                            animatedVisibilityScope = this@AnimatedVisibility
                        )
                )
                // ...
    }
    

    I made a sample with String keys that transitions from LazyColumn to HorizontalPager with navigation but as you can see neither screen are dependent of NavController.

    enter image description here

    @Composable
    private fun DetailsScreen(
        snack: SnackItem,
        sharedTransitionScope: SharedTransitionScope,
        animatedContentScope: AnimatedContentScope,
        onClick: (String) -> Unit,
    ) {
    
        var selectedIndex by remember {
            mutableStateOf(listSnacks.indexOf(snack).coerceAtLeast(0))
        }
        with(sharedTransitionScope) {
    
            val pagerState = rememberPagerState(
                initialPage = selectedIndex,
                pageCount = {
                    listSnacks.size
                }
            )
    
            HorizontalPager(
                state = pagerState,
                pageSpacing = 16.dp
            ) { page ->
    
                selectedIndex = page
                val currentSnack = listSnacks[page]
    
                Column(
                    Modifier
                        .fillMaxSize()
                        .clickable(
                            interactionSource = remember {
                                MutableInteractionSource()
                            },
                            indication = null
                        ) {
                            onClick("home")
                        }
                ) {
                    Image(
                        painterResource(id = currentSnack.image),
                        contentDescription = currentSnack.description,
                        contentScale = ContentScale.Crop,
                        modifier = Modifier
                            .sharedElement(
                                sharedTransitionScope.rememberSharedContentState(key = "image-$selectedIndex"),
                                animatedVisibilityScope = animatedContentScope
                            )
                            .aspectRatio(1f)
                            .fillMaxWidth()
                    )
                    Text(
                        currentSnack.name, fontSize = 38.sp,
                        modifier =
                        Modifier
                            .sharedBounds(
                                sharedTransitionScope.rememberSharedContentState(key = "text-$selectedIndex"),
                                animatedVisibilityScope = animatedContentScope,
                            )
                    )
                }
            }
        }
    }
    
    @Composable
    private fun HomeScreen(
    
        scrollState: LazyListState,
        sharedTransitionScope: SharedTransitionScope,
        animatedContentScope: AnimatedContentScope,
        onClick: (String) -> Unit,
    ) {
    
        var isDetailSelected by remember {
            mutableStateOf(false)
        }
    
        with(sharedTransitionScope) {
            Column {
    
                TopAppBar(
                    title = {
                        Text("Title")
                    },
                    backgroundColor = Color.Black,
                    contentColor = Color.White,
                    modifier = if (isDetailSelected) Modifier
                    else Modifier.renderInSharedTransitionScopeOverlay()
                )
                LazyColumn(
                    modifier = Modifier
                        .fillMaxSize(),
                    contentPadding = PaddingValues(8.dp),
                    state = scrollState,
                    verticalArrangement = Arrangement.spacedBy(8.dp)
                ) {
                    itemsIndexed(listSnacks) { index, item ->
                        Row(
                            Modifier.clickable(
                                interactionSource = remember {
                                    MutableInteractionSource()
                                },
                                indication = null
                            ) {
                                isDetailSelected = true
                                onClick("details/$index")
                            }
                        ) {
                            Spacer(modifier = Modifier.width(8.dp))
                            Image(
                                painterResource(id = item.image),
                                contentDescription = item.description,
                                contentScale = ContentScale.Crop,
                                modifier = Modifier
                                    .sharedElement(
                                        sharedTransitionScope.rememberSharedContentState(key = "image-$index"),
                                        animatedVisibilityScope = animatedContentScope
                                    )
                                    .size(120.dp)
                            )
                            Spacer(modifier = Modifier.width(8.dp))
                            Text(
                                item.name, fontSize = 18.sp,
                                modifier = Modifier
                                    .align(Alignment.CenterVertically)
                                    .sharedBounds(
                                        sharedTransitionScope.rememberSharedContentState(key = "text-$index"),
                                        animatedVisibilityScope = animatedContentScope
                                    )
                            )
                        }
                    }
                }
            }
        }
    }