Reputation: 12283
Here's the example how it works in AppleWallet: https://www.youtube.com/shorts/X-vcgG-WBCk
Basically the move animation happens like this: first the card smoothly moves down and then up
So I have made Jetpack Compose sample example (full code) with this reorderable library (edited a couple of code lines), but for now it uses default Modifier.animateItem()
animation for items:
private val CARD_HEIGHT = 500.dp
@Composable
fun ScreenContent(modifier: Modifier = Modifier) {
var items by remember { mutableStateOf((0..5).map { getRandomColor() }) }
val lazyListState = rememberLazyListState()
val cardPctHeight = 0.1f
val cardLastPctHeight = 0.5f
// https://github.com/Calvin-LL/Reorderable
val reorderableLazyListState = rememberReorderableLazyListState(
lazyListState = lazyListState,
shouldItemMoveThreshold = (CARD_HEIGHT * cardPctHeight) / 2f
) { from, to ->
items = items.toMutableList().apply {
add(to.index, removeAt(from.index))
}
}
LazyColumn(
state = lazyListState,
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(top = 10.dp),
verticalArrangement = Arrangement.Bottom
) {
itemsIndexed(
items = items,
key = { _, item -> item.toArgb() }
) { index, item ->
ReorderableItem(reorderableLazyListState, key = item.toArgb()) {
Box(
modifier = Modifier
.padding(horizontal = 10.dp)
.draggableHandle()
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val placeableHeight = if (index != items.lastIndex) {
placeable.height * cardPctHeight
} else {
placeable.height * cardLastPctHeight
}
layout(placeable.width, placeableHeight.roundToInt()) {
placeable.place(0, 0)
}
}
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.height(CARD_HEIGHT)
.background(color = item)
) {
//
}
}
}
}
}
private fun getRandomColor(): Color {
return Color(
red = Random.nextFloat(),
green = Random.nextFloat(),
blue = Random.nextFloat(),
alpha = 1f
)
}
Right now it works like this with the default animation:
https://youtube.com/shorts/gMDc6acLGrg
That reorderable library specifies default animation here
@Composable
fun LazyItemScope.ReorderableItem(
...
animateItemModifier: Modifier = Modifier.animateItem(),
UPDATE:
I was able to get the same animation (with exit and enter behavior) using AnimatedVisibility
(instead of LazyItemScope.animateItem
) but it only works normally if we dragging the item slowly, because there are Coroutine delays for the animation switching (enter/exit), and also if we move the dragging item faster we can see that other items are clipped to their visible height when they are moved.
Video: https://youtube.com/shorts/E_2Edsgz7Cg
Code:
private val CARD_HEIGHT = 500.dp
@Composable
fun AnimatedReorderableItem(
visible: Boolean,
content: @Composable () -> Unit
) {
AnimatedVisibility(
visible = visible,
enter = slideInVertically(
// The item slides in from below
initialOffsetY = { fullHeight -> fullHeight }
) + fadeIn(animationSpec = tween(durationMillis = 300)),
exit = slideOutVertically(
// The item slides out downward
targetOffsetY = { fullHeight -> fullHeight }
) + fadeOut(animationSpec = tween(durationMillis = 300))
) {
content()
}
}
@Composable
fun ScreenContent(modifier: Modifier = Modifier) {
Column {
var items by remember { mutableStateOf((0..5).map { getRandomColor() }) }
val itemVisibility = remember { mutableStateMapOf<Color, Boolean>() }
val lazyListState = rememberLazyListState()
val cardPctHeight = 0.1f
val cardLastPctHeight = 0.5f
// https://github.com/Calvin-LL/Reorderable
val reorderableLazyListState = rememberReorderableLazyListState(
lazyListState = lazyListState,
shouldItemMoveThreshold = (CARD_HEIGHT * cardPctHeight) / 2f
) { from, to ->
val item = items[to.index]
itemVisibility[item] = false
// TODO: but the problem here, we can't move the dragging item too fast (before animation ends)
delay(300)
itemVisibility[item] = true
items = items.toMutableList().apply {
add(to.index, removeAt(from.index))
}
}
LazyColumn(
state = lazyListState,
modifier = modifier
.fillMaxWidth()
.weight(1f),
contentPadding = PaddingValues(top = 10.dp),
verticalArrangement = Arrangement.Bottom
) {
itemsIndexed(
items = items,
key = { _, item -> item.toArgb() }
) { index, item ->
AnimatedReorderableItem(visible = itemVisibility[item] ?: true) {
ReorderableItem(
state = reorderableLazyListState,
key = item.toArgb(),
animateItemModifier = Modifier // disable default animation Modifier.animateItem()
) {
Box(
modifier = Modifier
.padding(horizontal = 10.dp)
.longPressDraggableHandle()
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val placeableHeight = if (index != items.lastIndex) {
placeable.height * cardPctHeight
} else {
placeable.height * cardLastPctHeight
}
layout(placeable.width, placeableHeight.roundToInt()) {
placeable.place(0, 0)
}
}
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.height(CARD_HEIGHT)
.background(color = item)
) {
//
}
}
}
}
}
}
}
As I understand the Modifier.animateItem()
is limited and only allows one transition animation for placement, we can't just set animation for item enter/exit as with AnimatedVisiblity
.
Upvotes: 0
Views: 73