Search code examples
androidkotlinandroid-jetpack-composejetpack-compose-animationandroid-jetpack-compose-animation

LazyColumn swap item animation


I have a list with 10 items one of them have this elements "rankingCurrentPlace", "rankingPastPlace" and "isUser:true".

What i need to do its an animation on the lazycolumn if the api esponse is like this "isUser:true", "rankingPastPlace:3" , "rankingCurrentPlace:7"

i need to show an animation in the list where the row starts in the third place and descend to the seventh place

is there a way to do this?

this is what I actually have

    LazyColumn(
        contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),

    ) {
        items(
            items = leaderboard,
            key = { leaderBoard ->
                leaderBoard.rankingPlace
            }
        ) { leaderBoard ->
                RowComposable( modifier = Modifier
                    .fillMaxWidth(),
                    topicsItem = leaderBoard,)                
        }

Solution

  • This answer works except when swapping first item with any item even with basic swap function without animation. I think it would be better to ask a new question about why swapping first item doesn't work or if it is bug. Other than that works as expected. If you need to move to items that are not in screen you can lazyListState.layoutInfo.visibleItemsInfo and compare with initial item and scroll to it before animation

    1.Have a SnapshotStateList of data to trigger recomposition when we swap 2 items

    class MyData(val uuid: String, val value: String)
    
    val items: SnapshotStateList<MyData> = remember {
        mutableStateListOf<MyData>().apply {
            repeat(20) {
                add(MyData( uuid = UUID.randomUUID().toString(), "Row $it"))
            }
        }
    }
    

    2.Function to swap items

    private fun swap(list: SnapshotStateList<MyData>, from: Int, to: Int) {
        val size = list.size
        if (from in 0 until size && to in 0 until size) {
            val temp = list[from]
            list[from] = list[to]
            list[to] = temp
        }
    }
    

    3.Function to swap items one by one. There is a bug with swapping first item. Even if it's with function above when swapping first item other one moves up without showing animation via Modififer.animateItemPlacement().

    @Composable
    private fun animatedSwap(
        lazyListState: LazyListState,
        items: SnapshotStateList<MyData>,
        from: Int,
        to: Int,
        onFinish: () -> Unit
    ) {
    
        LaunchedEffect(key1 = Unit) {
    
            val difference = from - to
            val increasing = difference < 0
    
            var currentValue: Int = from
    
    
            repeat(abs(difference)) {
    
                val temp = currentValue
    
                if (increasing) {
                    currentValue++
                } else {
                    currentValue--
                }
    
    
                swap(items, temp, currentValue)
                if (!increasing && currentValue == 0) {
                    delay(300)
                    lazyListState.scrollToItem(0)
                }
                delay(350)
    
            }
            onFinish()
        }
    }
    

    4.List with items that have Modifier.animateItemPlacement()

    val lazyListState = rememberLazyListState()
    LazyColumn(
        modifier = Modifier
            .fillMaxWidth()
            .weight(1f),
        state = lazyListState,
        contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
        verticalArrangement = Arrangement.spacedBy(4.dp)
    ) {
        items(
            items = items,
            key = {
                it.uuid
            }
        ) {
            Row(
                modifier = Modifier
    
                    .animateItemPlacement(
                        tween(durationMillis = 200)
                    )
                    .shadow(1.dp, RoundedCornerShape(8.dp))
                    .background(Color.White)
                    .fillMaxWidth()
                    .padding(8.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                Image(
                    modifier = Modifier
                        .clip(RoundedCornerShape(10.dp))
                        .size(50.dp),
                    painter = painterResource(id = R.drawable.landscape1),
                    contentScale = ContentScale.FillBounds,
                    contentDescription = null
                )
                Spacer(modifier = Modifier.width(10.dp))
                Text(it.value, fontSize = 18.sp)
            }
        }
    }
    

    Demo

    @OptIn(ExperimentalFoundationApi::class)
    @Composable
    private fun AnimatedList() {
        Column(modifier = Modifier.fillMaxSize()) {
    
            val items: SnapshotStateList<MyData> = remember {
                mutableStateListOf<MyData>().apply {
                    repeat(20) {
                        add(MyData(uuid = UUID.randomUUID().toString(), "Row $it"))
                    }
                }
            }
    
            val lazyListState = rememberLazyListState()
    
            LazyColumn(
                modifier = Modifier
                    .fillMaxWidth()
                    .weight(1f),
                state = lazyListState,
                contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
                verticalArrangement = Arrangement.spacedBy(4.dp)
            ) {
                items(
                    items = items,
                    key = {
                        it.uuid
                    }
                ) {
                    Row(
                        modifier = Modifier
    
                            .animateItemPlacement(
                                tween(durationMillis = 200)
                            )
                            .shadow(1.dp, RoundedCornerShape(8.dp))
                            .background(Color.White)
                            .fillMaxWidth()
                            .padding(8.dp),
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        Image(
                            modifier = Modifier
                                .clip(RoundedCornerShape(10.dp))
                                .size(50.dp),
                            painter = painterResource(id = R.drawable.landscape1),
                            contentScale = ContentScale.FillBounds,
                            contentDescription = null
                        )
                        Spacer(modifier = Modifier.width(10.dp))
                        Text(it.value, fontSize = 18.sp)
                    }
                }
            }
    
            var fromString by remember {
                mutableStateOf("7")
            }
    
            var toString by remember {
                mutableStateOf("3")
            }
    
            var animate by remember { mutableStateOf(false) }
    
    
    
            if (animate) {
    
                val from = try {
                    Integer.parseInt(fromString)
                } catch (e: Exception) {
                    0
                }
    
                val to = try {
                    Integer.parseInt(toString)
                } catch (e: Exception) {
                    0
                }
    
                animatedSwap(
                    lazyListState = lazyListState,
                    items = items,
                    from = from,
                    to = to
                ) {
                    animate = false
                }
            }
    
            Row(modifier = Modifier.fillMaxWidth()) {
    
                TextField(
                    value = fromString,
                    onValueChange = {
                        fromString = it
                    },
                    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
                )
    
                TextField(
                    value = toString,
                    onValueChange = {
                        toString = it
                    },
                    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
                )
    
            }
    
            Button(
                modifier = Modifier
                    .padding(8.dp)
                    .fillMaxWidth(),
                onClick = {
                    animate = true
                }
            ) {
                Text("Swap")
            }
        }
    }
    

    Edit: Animating with Animatable

    Another method for animating is using Animatable with Integer vector.

    val IntToVector: TwoWayConverter<Int, AnimationVector1D> =
        TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() })
    
    val coroutineScope = rememberCoroutineScope()
    val animatable = remember { Animatable(0, IntToVector) }
    

    And can be used as

    private fun alternativeAnimate(
        from: Int,
        to: Int,
        coroutineScope: CoroutineScope,
        animatable: Animatable<Int, AnimationVector1D>,
        items: SnapshotStateList<MyData>
    ) {
        
        val difference = from - to
        var currentValue: Int = from
    
        coroutineScope.launch {
            animatable.snapTo(from)
    
            animatable.animateTo(to,
                tween(350 * abs(difference), easing = LinearEasing),
                block = {
                    val nextValue = this.value
                    if (abs(currentValue -nextValue) ==1) {
                        swap(items, currentValue, nextValue)
                        currentValue = nextValue
                    }
                }
            )
        }
    }
    

    on button click, i'm getting values from TextField fo i convert from String

        Button(
            modifier = Modifier
                .padding(8.dp)
                .fillMaxWidth(),
            onClick = {
                val from = try {
                    Integer.parseInt(fromString)
                } catch (e: Exception) {
                    0
                }
    
                val to = try {
                    Integer.parseInt(toString)
                } catch (e: Exception) {
                    0
                }
                alternativeAnimate(from, to, coroutineScope, animatable, items)
            }
        ) {
            Text("Swap")
        }
    

    Result

    enter image description here