skydoves / Orbital

🪐 Jetpack Compose Multiplatform library that allows you to implement dynamic transition animations such as shared element transitions.
Apache License 2.0
1.08k stars 35 forks source link

Using multiple separately remembered orbital-scoped elements in the same Orbital scope causes issues in rendering #26

Closed Martmists-GH closed 7 months ago

Martmists-GH commented 8 months ago

Please complete the following information:

Describe the Bug: Peek 2023-11-08 00-27 All entries become invisible and the first location is used as the source location.

Expected Behavior:

Only the selected entry moves

Code snippet

package com.martmists.bbs.app.component

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.dp
import com.martmists.bbsp.graphql.client.OwnedCardGraphQL
import com.skydoves.orbital.Orbital
import com.skydoves.orbital.OrbitalScope
import com.skydoves.orbital.animateSharedElementTransition
import com.skydoves.orbital.rememberContentWithOrbitalScope

@Composable
fun CardGrid(cards: List<OwnedCardGraphQL>) {
    val itemWidth = 256
    val padding = 5

    var width by remember { mutableStateOf(1) }
    var selected by remember { mutableStateOf(-1) }
    val cardIsSelected by derivedStateOf { selected >= 0 }

    // I want to avoid recreating these entries as much as possible, hence the below hack
    val cardRenders = remember {
        Array<@Composable OrbitalScope.() -> Unit>(cards.size) { { } }
    }

    cards.forEachIndexed { i, it ->
        val item = rememberContentWithOrbitalScope {
            CardRender(
                modifier = Modifier
                    .animateSharedElementTransition(this)
                    .clickable(!cardIsSelected) {
                        selected = i
                    },
                card = it
            )
        }
        cardRenders[i] = item
    }

    Orbital {
        // Grid of items
        LazyVerticalGrid(
            columns = GridCells.Adaptive(minSize = itemWidth.dp),
            horizontalArrangement = Arrangement.spacedBy(padding.dp),
            verticalArrangement = Arrangement.spacedBy(15.dp),
            modifier = Modifier
                .onGloballyPositioned {
                    width = it.size.width
                }
                .fillMaxSize()
        ) {
            itemsIndexed(cards, key = { i, it -> i }) { i, it ->
                Box {
                    // Blank placeholder
                    CardRender()
                    // Card
                    if (!cardIsSelected) {
                        cardRenders[i]()
                    }
                }
            }

            // Fill last row with blank placeholders
            val ncol = (width) / (itemWidth + padding)
            val remain = ncol - (cards.size % ncol)
            if (remain != ncol) {
                items(remain) {
                    CardRender()
                }
            }
        }

        // Fullscreen preview
        if (cardIsSelected) {
            Column(
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.End,
                modifier = Modifier
                    .clickable {
                        selected = -1
                    }
                    .fillMaxSize()
            ) {
                if (cardIsSelected) {
                    cardRenders[selected]()
                }
            }
        }
    }
}
skydoves commented 8 months ago

Hey @Martmists-GH, does this issue happen only with the desktop and web? If I'm correct, we can't use LookaheadLayout with the LazyColumn yet, and it will be supported in the future Compose. Let me keep you updated on this if I get any news.

Martmists-GH commented 8 months ago

Android is also affected, and regular Columns and Rows are also affected by this.

skydoves commented 8 months ago

Hey @Martmists-GH, the new release (0.3.2) introduced new functions to achieve this. Please check out the documentation: https://github.com/skydoves/orbital#shared-element-transition-with-lazylist

Martmists-GH commented 8 months ago

Hmm, I haven't quite gotten it to work with that. Here's my new code:


@Composable
fun CardGrid(cards: List<OwnedCardGraphQL>) {
    val itemWidth = 256
    val padding = 5

    var width by remember { mutableStateOf(1) }
    var selected by remember { mutableStateOf(-1) }
    val cardIsSelected by derivedStateOf { selected >= 0 }

    val cardRenders = remember {
        // No longer scoped to OrbitalScope
        Array<@Composable () -> Unit>(cards.size) { { } }
    }

    Orbital {
        // Remember cards in Orbital scope
        cards.forEachIndexed { i, it ->
            val item = rememberMovableContentOf {
                CardRender(
                    modifier = Modifier
                        .animateBounds()  // replaces animateSharedElementTransition(this)
                        .clickable(!cardIsSelected) {
                            selected = i
                        },
                    card = it
                )
            }
            cardRenders[i] = item
        }

        // Row/Grid of cards
        Row {
            cards.forEachIndexed { i, it ->
                Box {
                    CardRender()
                    if (!cardIsSelected) {
                        Orbital {  // Now in new Orbital, mirroring the example
                            cardRenders[i]()
                        }
                    }
                }
            }
        }

        // Fullscreen preview (overlay) of selected card
        if (cardIsSelected) {
            Column(
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.End,
                modifier = Modifier
                    .clickable {
                        selected = -1
                    }
                    .fillMaxSize()
            ) {
                if (cardIsSelected) {
                    Orbital {  // Now in new Orbital, mirroring the example
                        cardRenders[selected]()
                    }
                }
            }
        }
    }
}
Martmists-GH commented 8 months ago

It seems to be caused by nested elements, here is a reproducer

@Composable
fun Example() {
    var expanded by rememberSaveable { mutableStateOf(false) }
    val content = rememberContentWithOrbitalScope {
        Text("Sample Text", modifier=Modifier.animateSharedElementTransition(this))
    }

    Orbital {
        if (expanded) {
            Column(modifier = Modifier.fillMaxSize()) {
                content()
                Button(onClick = {
                    expanded = false
                }) {
                    Text("Toggle")
                }
            }
        } else {
            Row(modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
                // Column {  // Uncomment this to make orbital animations stop working
                    content()
                    Button(onClick = {
                        expanded = true
                    }) {
                        Text("Toggle")
                    }
                // }
            }
        }
    }
}