Thread
#compose
    o

    Olivier Patry

    1 year ago
    I try to animate a transition between current data state and previous one. I have difficulties to put everything in place in my mind to achieve the transition considering the previous state. I read https://developer.android.com/jetpack/compose/animation but it didn't help so far. I have a 4x4 grid compound of cells. I can move cells in all directions (imagine a 2048-like game). I'd like to do 3 transitions:1. animate "slide" of tiles 2. animate "transform" of tiles (2 identical tiles are merged together creating a new one with higher value) 3. new tile appearance (one new tile appears at each move) I struggle on how to achieve this…
    (for those who don't know 2048, here is an online example https://play2048.co/)
    To start playing with animation I did this
    val anim = remember {
            TargetBasedAnimation(
                animationSpec = tween(200),
                typeConverter = Float.VectorConverter,
                initialValue = 0f,
                targetValue = 360f
            )
        }
        var playTime by remember { mutableStateOf(0L) }
    
        // FIXME only if both previous and current tile were non empty tiles
        LaunchedEffect(tile) {
            val startTime = withFrameNanos { it }
    
            do {
                playTime = withFrameNanos { it } - startTime
                val animationValue = anim.getValueFromNanos(playTime)
            } while (!anim.isFinishedFromNanos(playTime))
        }
    ...
    
        Modifier.rotate(anim.getValueFromNanos(playTime))
    It "kinda" work but isn't really accurate. Despite using
    tile.id
    (a unique ID associated to each tile), too many tiles are animated.
    and I still don't understand how to apply a tile offset transition from previous state to current
    Doris Liu

    Doris Liu

    1 year ago
    Could you post your code for moving the tile without animation? The animation would be easy to add if the non-animating code is already structured in a declarative way.
    and I still don't understand how to apply a tile offset transition from previous state to current
    Generally speaking, this can be achieved by creating a stateful animation (e.g. animate*AsState, Animatable) to store the value calculated from the previous state, so that when state changes the new value can be used as the new target for the stateful animation.
    o

    Olivier Patry

    1 year ago
    @Doris Liu Usually, I can manage such stateful animations and I thought I was understanding what is going on, but for this use case I'm stuck, maybe the model of compose animation I have in mind is wrong 😅 With
    animate*AsState
    , my understanding is that a link is made between the animation spec and the animated entity, thus automatically managing the "from previous to current state" logic. Here, I'm stuck because the animation logic doesn't seem that easy to express.
    Here follows my current "static" impl. The grid:
    @Composable
    fun TilesGrid(
        tiles: List<Tile>,
        size: IntSize) {
        LazyVerticalGrid(GridCells.Fixed(size.width)) {
            items(tiles) { tile ->
                TileView(tile)
            }
        }
    }
    The tile:
    @Composable
    fun TileView(tile: Tile) {
        Box {
            // only draw tile slot for empty tiles
            if (tile != Tile.EMPTY_TILE) {
                val color by animateColorAsState(tile.color)
                Box(
                    Modifier
                        .padding(bottom = 8.dp, start = 2.dp, end = 2.dp)
                        .background(color, RoundedCornerShape(4.dp))
                        .fillMaxSize()
                        .padding(bottom = 4.dp),
                    contentAlignment = Alignment.BottomCenter
                ) {
                    Text(tile.value.toString())
                }
            }
        }
    }
    And my
    Tile
    class is
    data class Tile(val rank: Int) : Comparable<Tile> {
        val id: Long = ID++
        val value: Int = 2.0.pow(rank).toInt()
    
        override fun compareTo(other: Tile): Int {
            return rank - other.rank
        }
    
        companion object {
            private var ID: Long = 0L
    
            val EMPTY_TILE = Tile(0)
        }
    }
    With sample data, we should have various cases to handle:
    val tiles1 = listOf(
        Tile.EMPTY_TILE, Tile(3), Tile.EMPTY_TILE, Tile.EMPTY_TILE,
        Tile.EMPTY_TILE, Tile(2), Tile.EMPTY_TILE, Tile(2),
        Tile.EMPTY_TILE, Tile(2), Tile(3), Tile.EMPTY_TILE,
        Tile.EMPTY_TILE, Tile.EMPTY_TILE, Tile(2), Tile(1),
    )
    val tiles2 = listOf(
        Tile.EMPTY_TILE, tiles1[1], Tile.EMPTY_TILE, tiles1[7],
        Tile.EMPTY_TILE, Tile(3), tiles1[10], Tile.EMPTY_TILE,
        Tile.EMPTY_TILE, Tile.EMPTY_TILE, tiles1[14], tiles1[15],
        Tile.EMPTY_TILE, Tile(1), Tile.EMPTY_TILE, Tile.EMPTY_TILE,
    )
    
    @ExperimentalFoundationApi
    @Composable
    fun Playground() {
        var state by remember { mutableStateOf(true) }
        val tiles = if (state) tiles1 else tiles2
    
        TilesGrid2(tiles, IntSize(4, 4)) {
            state = !state
        }
    }
    On click on any tile, it simulates 1 movement of tiles from bottom to top merging identical tiles and inserting a new one.
    I bootstrapped an animation to showcase it:
    val scale = remember(tile.id) { Animatable(0f) }
    LaunchedEffect(tile.id) {
        scale.animateTo(
            targetValue = 1f,
            animationSpec = spring(
                dampingRatio = Spring.DampingRatioMediumBouncy,
                stiffness = Spring.StiffnessMedium,
                visibilityThreshold = .3f
            )
        )
    }
    And applying such scale to tile
    I guess that with the following code, a full reproducible example should be complete
    In this configuration Stable tiles (same
    tile.id
    same position) do not animate (which is expected). New ones (new or result of a merge) are animated (which is expected, new
    tile.id
    ). But tiles that are simply moving from one cell to another are animated which isn't expected. The
    tile.id
    being the same, I would have expected stable state. I guess this comes from the fact that such
    TileView
    composable is managed by
    LazyVerticalGrid
    ?
    Doris Liu

    Doris Liu

    1 year ago
    Yea, this static implementation is indeed difficult to convert to a state-based animation. 🙂 The reason is, for tiles to know where they should be moving towards, each tile needs to retain identity. In your impl, the tiles themselves are stateless. I would recommend making two changes to your impl above: 1) Make the tile stateful so they remember the position on the board they are placed, maybe by adding position field to the Tile class. This way when that position gets updated after a swipe it's pretty straightforward to animate. 2) Use a custom layout instead of a lazy layout, as then you can more freely control the position of the tiles via
    Modifier.offset
    or
    place(...)
    .
    o

    Olivier Patry

    1 year ago
    Okay I guess I see what to do now. I'll try to
    map
    my tiles 2D board to a list of
    TileState
    with the board position in a mutable state. I'm not sure about the details yet but you gave me a good lead on where to dig! Thanks
    @Doris Liu small update on this. I wasn't able to work on it recently, but I took some time today to try your advice. I still have things to tweak but it sounds promising! I defined a stable ID for each
    Tile
    . I store the last update made on each tile (new, none, combination, top, bottom, left, right). When I update my board, I can determine such new state for each tile. This way, I can start playing with animation depending on the update state and distinguish the different cases. I have remaining issues related to origin of animations (when dealing with rotate + offset) but I'll keep going. I think I have stronger foundation to complete my goal now 👍 Thanks 🙇
    Doris Liu

    Doris Liu

    1 year ago
    Thanks for the update. Glad the new direction is promising. 🙂
    o

    Olivier Patry

    1 year ago
    I still have to investigate your suggestion about getting rid of
    LazyVerticalGrid
    . Maybe I'll have to define a
    TileState
    containing my tile data, animation state and expected position in grid. Then draw list of
    TileState
    given such position and defined animation states. I could address the issue I currently have with animation combination.
    Doris Liu

    Doris Liu

    1 year ago
    In the case of a 4x4 grid, all the cells will be showing on screen, therefore not much need to have it be Lazy. Or is it due to the lack of a non-lazy grid component?
    o

    Olivier Patry

    1 year ago
    I was lazy myself 😅 I didn't thought about the scalability issue, just by habits: for horizontal list
    LazyRow
    , for vertical list
    LazyColumn
    , for grid,
    LazyVerticalGrid
    🙃
    I still have plenty of things to clean but it starts to work like I expected 😌 (TODO: anim from out of board bounds, z-order issue, cleaning code, tweaking what to animate and how (currently dummy property update), …)
    (clone of an "old" game: Hues (now unavailable))
    Doris Liu

    Doris Liu

    1 year ago
    Nice! Thanks for sharing!🎉