Jetpack Compose 列表动画即拿即用

2,656 阅读4分钟

效果图

淡入缩放左边进入
淡入.gif缩放.gif左边进入.gif

前言

右边进入右边进入+弹簧效果淡入+缩放
右边进入.gif右边进入+弹簧效果.gif淡入+缩放.gif

关于Compose动画学习部分可以移步到其他大佬的文章 安安安 - Compose 动画详解,我demo内也有专门的动画API实战页面。

WechatIMG61_gaitubao_376x837.jpeg

使用与关键代码

将下面动画效果的modifier传递给列表子视图最外层的Modifier调用即可,下面是伪代码:

//下方的淡入代码copy到这里来

LazyColumn() {
    itemsIndexed() {
        TestScreen() {
            Row(modifier.fillMaxWidth().height(80.dp)) {
            
            }
        }
    }
}

淡入

val animated = remember {
    Animatable(0f)
}
LaunchedEffect(key1 = Unit, block = {
    animated.animateTo(1f, tween(800))
})

val modifier = Modifier.graphicsLayer(alpha = animated.value)

缩放

val animated = remember {
    Animatable(0.5f)
}

LaunchedEffect(key1 = Unit, block = {
    animated.animateTo(1f, tween(800, easing = LinearEasing))
})

val modifier = Modifier.graphicsLayer(scaleY = animated.value, scaleX = animated.value)

左边进入

val animated = remember {
    Animatable(-300f)
}

LaunchedEffect(key1 = Unit, block = {
    animated.animateTo(
        0f, tween(
            800, easing = FastOutSlowInEasing
        )
    )
})

val modifier = Modifier.graphicsLayer(translationX = animated.value)

右边进入

val animated = remember {
    Animatable(300f)
}

LaunchedEffect(key1 = Unit, block = {
    animated.animateTo(
        0f, tween(
            800, easing = FastOutSlowInEasing
        )
    )
})

val modifier = Modifier.graphicsLayer(translationX = animated.value)

右边进入 + 弹簧效果

val animated = remember {
    Animatable(300f)
}

LaunchedEffect(key1 = Unit, block = {
    animated.animateTo(
        0f, spring(
            //阻尼
            dampingRatio = Spring.DampingRatioMediumBouncy,
            //坚硬度
            stiffness = Spring.StiffnessLow
        )
    )
})

val modifier = Modifier.graphicsLayer(translationX = animated.value)

淡入 + 缩放

改造前

val animated = remember {
    Animatable(0.2f)
}

val alphaAnimated = remember {
    Animatable(0.5f)
}

LaunchedEffect(key1 = Unit, block = {
    animated.animateTo(1f, tween(600, easing = LinearEasing))
    alphaAnimated.animateTo(1f, tween(1000))
})

val modifier = Modifier.graphicsLayer(
    scaleX = animated.value,
    scaleY = animated.value,
    alpha = alphaAnimated.value
)

由于在Compose1.0.5版本内,Animatable方法的initialValue参数不是泛型,这导致我们在写组合动画效果的时候需要定义多个Animatable(或者使用animateValueAsState也可以实现,但使用时相对麻烦一些)。

Animatable类的initialValue参数是泛型,所以我们只需简单加以改造就可以使用了。

fun <T, V: AnimationVector> AnimatableTwoWay(
    initialValue: T,
    typeConverter: TwoWayConverter<T, V>
) = Animatable(initialValue, typeConverter, null)

还需要搭配一个数据类,例如

data class TwoWayData(val value1: Float, val value2: Float)

改造后

看起来代码量和改造前差不多,但此时initialValue再多我们的代码量也不会增加多少了。

val animated2 = remember {
    AnimatableTwoWay(TwoWayData(0.2f, 0.5f), TwoWayConverter(
        convertToVector = {
            AnimationVector2D(it.value1, it.value2)
        }, convertFromVector = {
            TwoWayData(it.v1, it.v2)
        }
    ))
}

LaunchedEffect(key1 = Unit, block = {
    animated2.animateTo(TwoWayData(1f, 1f), tween(800))
})

Modifier.graphicsLayer(
    scaleX = animated2.value.value1,
    scaleY = animated2.value.value1,
    alpha = animated2.value.value2
)

不过现在只能传入一个animationSpec,有没有大佬再改造一下...

可以copy下来直接预览的代码

import androidx.annotation.DrawableRes
import androidx.annotation.Keep
import androidx.compose.animation.core.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController

val listAnimateEnum by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
    mutableStateOf(ListAnimateEnum.FADE_IN)
}

@Keep
data class ListAnimateData(
    val title: String,
    val content: String,
    @DrawableRes val imgId: Int
)

enum class ListAnimateEnum(val i: Int) {
    //淡入
    FADE_IN(0),

    //缩放
    ZOOM(1),

    //左边进入
    LEFT_IN(2),

    //右边进入
    RIGHT_IN(3),

    //右边进入 + 弹簧效果
    RIGHT_IN_SPRING(4),

    //淡入 + 缩放
    FADE_IN_ZOOM(5)
}

val menusList = listOf(
    "淡入",
    "缩放",
    "左边进入",
    "右边进入",
    "右边进入 + 弹簧效果",
    "淡入 + 缩放"
)

private val data = ListAnimateData(
    "啊巴啊巴1",
    "啊巴啊巴啊巴啊巴啊巴啊巴啊巴啊巴啊巴啊巴啊巴啊巴啊巴啊巴啊巴啊巴",
    R.drawable.ic_launcher_background
)

val animateList = listOf(
    data,
    data.copy(title = "啊巴啊巴2"),
    data.copy(title = "啊巴啊巴3"),
    data.copy(title = "啊巴啊巴4"),
    data.copy(title = "啊巴啊巴5"),
    data.copy(title = "啊巴啊巴6"),
    data.copy(title = "啊巴啊巴7"),
    data.copy(title = "啊巴啊巴8"),
    data.copy(title = "啊巴啊巴9"),
    data.copy(title = "啊巴啊巴10"),
    data.copy(title = "啊巴啊巴11"),
    data.copy(title = "啊巴啊巴12"),
    data.copy(title = "啊巴啊巴13"),
    data.copy(title = "啊巴啊巴14"),
    data.copy(title = "啊巴啊巴15"),
    data.copy(title = "啊巴啊巴16"),
    data.copy(title = "啊巴啊巴17"),
    data.copy(title = "啊巴啊巴18"),
    data.copy(title = "啊巴啊巴19"),
    data.copy(title = "啊巴啊巴20"),
    data.copy(title = "啊巴啊巴21"),
    data.copy(title = "啊巴啊巴22"),
    data.copy(title = "啊巴啊巴23"),
    data.copy(title = "啊巴啊巴24"),
    data.copy(title = "啊巴啊巴25"),
    data.copy(title = "啊巴啊巴26"),
    data.copy(title = "啊巴啊巴27"),
    data.copy(title = "啊巴啊巴28"),
    data.copy(title = "啊巴啊巴29"),
    data.copy(title = "啊巴啊巴30"),
)

/**
 * 列表动画
 */
@Composable
fun ListAnimateCompose(navHostController: NavHostController) {

    var dropExpanded by remember {
        mutableStateOf(false)
    }

    BaseScreen {
        Scaffold(
            topBar = {
                TopAppBar(
                    title = {
                        Text(text = "列表动画")
                    },
                    navigationIcon = {
                        IconButton(onClick = {
                            navHostController.navigateUp()
                        }) {
                            Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
                        }
                    },
                    actions = {
                        Box(contentAlignment = Alignment.Center) {
                            IconButton(onClick = {
                                dropExpanded = !dropExpanded
                            }) {
                                Icon(
                                    imageVector = Icons.Default.MoreVert,
                                    contentDescription = null
                                )
                            }
                            DropdownMenu(
                                expanded = dropExpanded,
                                onDismissRequest = { dropExpanded = false }) {
                                menusList.forEachIndexed { index, str ->
                                    TextButton(
                                        onClick = {
                                            listAnimateEnum.value = when(index) {
                                                0 -> ListAnimateEnum.FADE_IN
                                                1 -> ListAnimateEnum.ZOOM
                                                2 -> ListAnimateEnum.LEFT_IN
                                                3 -> ListAnimateEnum.RIGHT_IN
                                                4 -> ListAnimateEnum.RIGHT_IN_SPRING
                                                5 -> ListAnimateEnum.FADE_IN_ZOOM
                                                else -> ListAnimateEnum.FADE_IN
                                            }
                                            dropExpanded = false
                                        },
                                        modifier = Modifier
                                            .fillMaxWidth()
                                            .padding(top = 6.dp, bottom = 6.dp)
                                    ) {
                                        Text(text = str)
                                    }
                                }
                            }
                        }
                    },
                    backgroundColor = Color.White
                )
            },
            modifier = it.fillMaxSize()
        ) {
            LazyColumn(horizontalAlignment = Alignment.CenterHorizontally) {
                itemsIndexed(animateList) { _: Int, item: ListAnimateData ->
                    ListAnimateScreen(item.title, item.content, item.imgId)
                }
            }
        }
    }
}

fun <T, V: AnimationVector> AnimatableTwoWay(
    initialValue: T,
    typeConverter: TwoWayConverter<T, V>
) = Animatable(initialValue, typeConverter, null)

data class TwoWayData(val value1: Float, val value2: Float)

@Composable
private fun ListAnimateScreen(title: String, content: String, @DrawableRes mId: Int) {

    val modifier = when (listAnimateEnum.value.i) {
        //淡入
        0 -> {
            val animated = remember {
                Animatable(0f)
            }
            LaunchedEffect(key1 = Unit, block = {
                animated.animateTo(1f, tween(800))
            })

            Modifier.graphicsLayer(alpha = animated.value)
        }
        //放大
        1 -> {
            val animated = remember {
                Animatable(0.5f)
            }

            LaunchedEffect(key1 = Unit, block = {
                animated.animateTo(1f, tween(800, easing = LinearEasing))
            })

            Modifier.graphicsLayer(scaleY = animated.value, scaleX = animated.value)
        }
        //左边进入
        2 -> {
            val animated = remember {
                Animatable(-300f)
            }

            LaunchedEffect(key1 = Unit, block = {
                animated.animateTo(
                    0f, tween(
                        800, easing = FastOutSlowInEasing
                    )
                )
            })

            Modifier.graphicsLayer(translationX = animated.value)
        }
        //右边进入
        3 -> {
            val animated = remember {
                Animatable(300f)
            }

            LaunchedEffect(key1 = Unit, block = {
                animated.animateTo(
                    0f, tween(
                        800, easing = FastOutSlowInEasing
                    )
                )
            })

            Modifier.graphicsLayer(translationX = animated.value)
        }
        //右边进入+弹簧效果
        4 -> {
            val animated = remember {
                Animatable(300f)
            }

            LaunchedEffect(key1 = Unit, block = {
                animated.animateTo(
                    0f, spring(
                        //阻尼
                        dampingRatio = Spring.DampingRatioMediumBouncy,
                        //坚硬度
                        stiffness = Spring.StiffnessLow
                    )
                )
            })

            Modifier.graphicsLayer(translationX = animated.value)
        }
        //淡入 + 放大
        5 -> {
//            val animated2 = remember {
//                AnimatableTwoWay(TwoWayData(0.2f, 0.5f), TwoWayConverter(
//                    convertToVector = {
//                        AnimationVector2D(it.value1, it.value2)
//                    }, convertFromVector = {
//                        TwoWayData(it.v1, it.v2)
//                    }
//                ))
//            }

            val animated = remember {
                Animatable(0.2f)
            }

            val alphaAnimated = remember {
                Animatable(0.5f)
            }

            LaunchedEffect(key1 = Unit, block = {
                animated.animateTo(1f, tween(600, easing = LinearEasing))
                alphaAnimated.animateTo(1f, tween(1000))
//                animated2.animateTo(TwoWayData(1f, 1f), tween(800))
            })

            Modifier.graphicsLayer(
                scaleX = animated.value,
                scaleY = animated.value,
                alpha = alphaAnimated.value
            )
//            Modifier.graphicsLayer(
//                scaleX = animated2.value.value1,
//                scaleY = animated2.value.value1,
//                alpha = animated2.value.value2
//            )
        }
        else -> Modifier
    }

    ListAnimateItem(
        modifier,
        title,
        content,
        mId
    )
}

/**
 * 列表子项布局
 */
@Composable
private fun ListAnimateItem(
    modifier: Modifier,
    title: String,
    content: String,
    @DrawableRes mId: Int
) {
    Row(
        modifier = modifier
            .fillMaxWidth()
            .height(60.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Image(
            painter = painterResource(id = mId),
            contentDescription = null,
            modifier = Modifier
                .size(50.dp)
                .padding(start = 10.dp)
        )
        Column(
            verticalArrangement = Arrangement.Center,
            modifier = Modifier
                .fillMaxHeight()
                .padding(start = 6.dp)
        ) {
            Text(text = title)
            Text(
                text = content, maxLines = 1, overflow = TextOverflow.Ellipsis,
                color = Color.Gray,
                fontSize = 12.sp
            )
        }
    }
    Divider()
}

demo地址