LazyColumn swap item animation

445 Views Asked by At

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,)                
        }
2

There are 2 best solutions below

0
On BEST ANSWER

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

0
On

I suggest you to get your items from a data class. If your other items does not contain the variables you mentioned you can make them nullable in data class and put a condition checker in your lazycolumn

Like this

data class Items(
   val otherItems: Other,
   val rankingCurrentPlace: Int?,
   val rankingLastPlace: Int?,
   val isUser: Boolean?
)

Then you can make a list from this data class and pass it to lazycolumn

LazyColumn{
    items(list){
        (elements with condition)
    }
}