前言
此篇博客講解Jetpack Compose的動畫實現。Compose的動畫分兩種:
- 一種是可以簡單快捷使用的AnimatedVisibility 、AnimatedContent、Animatable動畫,他們已經將使用進行的簡單的封裝。這其中AnimatedVisibility(動畫控制顯示與隱藏) 和 AnimatedContent(動畫控制內容切換)已經封裝成了容器組件
- 另一種是稍微複雜的animateFloatAsState系列的屬性動畫,這個更底層可以完成更復雜組合的動畫。
此外除了上面的動畫函數,你還需要了解插值器函數,他們會控制動畫的執行中的效果或者持續時間,他們一共有3種:
- tween:補間插值器,這個插值器可以讓你設置動畫的持續時間,這是最常用的插值器
- spring: 彈跳插值器,spring可以實現類似彈簧或者彈跳的動畫效果,它不可以設置動畫時間,但是可以設置阻尼值以調整回彈效果
- keyframes:關鍵幀插值器,關鍵幀動畫可以逐幀設置當前動畫的軌跡
- snap:提前動畫,直接不執行動畫過程,直接到達動畫結果
官網文檔:https://developer.android.google.cn/jetpack/compose/animation?hl=zh-cn
Animatable與tween插值器
下面通過Animatable與tween插值器,實現了一個顏色漸變的動畫效果。
效果圖
代碼,代碼中使用了tween插值器,此外easing動畫數值曲線還有以下選擇:
- FastOutSlowInEasing 快出慢進
- FastOutLinearInEasing 先快出,後勻速線性進
- LinearOutSlowInEasing 先勻速線性出,後快進
- LinearEasing 勻速線性
@Composable
fun ColorAnimation() {
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(true) {
color.animateTo(Color.Green,
//delayMillis = 動畫開始延遲時間 , durationMillis = 動畫持續時間 , easing = 動畫數值曲線
animationSpec = tween(durationMillis = 3000, delayMillis = 1000, easing = LinearEasing)
)
}
Box(modifier = Modifier.fillMaxSize()) {
Surface(
color = color.value,
modifier = Modifier
.size(100.dp)
.align(Alignment.Center)
){}
}
}
spring彈跳插值器
下面通過Animatable與spring插值器,實現了一個掉落動畫。
請注意!下面的代碼中,設置動畫數值的函數是graphicsLayer。 這裏不建議直接使用offset或者scale等等這些函數來說實現動畫(可以在graphicsLayer裏調用offset),因爲直接offset會引起父類容器的重組,導致整個頁面都在重組刷新,有些時候還會引起移動時抖動或者拖影的現象。 這裏建議使用graphicsLayer會將重組範圍限制到當前組件裏,避免父類與其他組件都連帶重組。
效果圖
代碼
@Composable
fun FallOffAnimation() {
val yAnimation1 = remember { Animatable(0f) }
val yAnimation2 = remember { Animatable(0f) }
val yAnimation3 = remember { Animatable(0f) }
val yAnimation4 = remember { Animatable(0f) }
LaunchedEffect(true) {
delay(2000)
yAnimation1.animateTo(
200f,
//dampingRatio = 阻尼比 , stiffness = 剛度
//DampingRatioHighBouncy = 阻尼比高彈性 , StiffnessHigh = 剛度高
animationSpec = spring(dampingRatio = Spring.DampingRatioHighBouncy, stiffness = Spring.StiffnessHigh)
)
yAnimation2.animateTo(
200f,
//DampingRatioHighBouncy = 阻尼比高彈性 , StiffnessMediumLow = 剛度中低
animationSpec = spring(dampingRatio = Spring.DampingRatioHighBouncy, stiffness = Spring.StiffnessMediumLow)
)
yAnimation3.animateTo(
200f,
//DampingRatioMediumBouncy = 阻尼比中彈性 , StiffnessMedium = 剛度中
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessMediumLow)
)
//DampingRatioNoBouncy = 阻尼比無彈性 , StiffnessVeryLow = 剛度非常低
yAnimation4.animateTo(
200f,
animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessVeryLow)
)
}
Row(modifier = Modifier.fillMaxSize().padding(100.dp)) {
Surface(
color = Color.Green,
modifier = Modifier
.padding(horizontal = 30.dp)
.size(40.dp)
.graphicsLayer {
translationY = yAnimation1.value
}
) {}
Surface(
color = Color.Green,
modifier = Modifier
.padding(horizontal = 30.dp)
.size(40.dp)
.graphicsLayer {
translationY = yAnimation2.value
}
) {}
Surface(
color = Color.Green,
modifier = Modifier
.padding(horizontal = 30.dp)
.size(40.dp)
.graphicsLayer {
translationY = yAnimation3.value
}
) {}
Surface(
color = Color.Green,
modifier = Modifier
.padding(horizontal = 30.dp)
.size(40.dp)
.graphicsLayer {
translationY = yAnimation4.value
}
) {}
}
}
keyframes插值器
下面通過Animatable與keyframes插值器,通過自定義設置動畫幀的速度,實現了一個先快中間慢最後快的掉落動畫。
效果圖
代碼
@Composable
fun FallOffAnimation() {
val yAnimation1 = remember { Animatable(0f) }
LaunchedEffect(true) {
delay(2000)
yAnimation1.animateTo(
300f,
animationSpec = keyframes {
//動畫持續時間
durationMillis = 3000
/*
簡單說明下面代碼意思:
下面代碼實現了先以100毫秒從0f移動到100f的位置,然後以2800毫秒從100f移動到200f的位置,再接着以100毫秒從200f移動到最後的300f位置
*/
0f at 0 with FastOutLinearInEasing //從0f位置0毫秒開始以FastOutLinearInEasing數值曲線運行到下一個階段
100f at 100 with LinearEasing //經過了100毫秒運動到100f此位置,現在從100f位置以LinearEasing數值曲線運行到下一個階段
200f at 2900 with FastOutLinearInEasing //經過了2800毫秒運動到200f此位置,現在從200f位置以LinearEasing數值曲線運行到300f結束
}
)
}
Row(modifier = Modifier
.fillMaxSize()
.padding(100.dp)) {
Surface(
color = Color.Green,
modifier = Modifier
.padding(horizontal = 30.dp)
.size(40.dp)
.graphicsLayer {
translationY = yAnimation1.value
}
) {}
}
}
動畫的重複
repeatable:可重複
infiniteRepeatable:無限重複
效果圖
代碼
@Composable
fun RotationAnimation() {
val zRotationAnimation1 = remember { Animatable(0f) }
val zRotationAnimation2 = remember { Animatable(0f) }
//repeatable 設置重複次數
LaunchedEffect(true) {
zRotationAnimation1.animateTo(
360f,
//iterations 重複次數
animationSpec = repeatable(iterations = 2, animation = tween(durationMillis = 1000))
)
}
//infiniteRepeatable 無限重複
LaunchedEffect(true) {
zRotationAnimation2.animateTo(
360f,
animationSpec = infiniteRepeatable(animation = tween(durationMillis = 1000))
)
}
Row(
modifier = Modifier
.fillMaxSize()
.padding(100.dp)
) {
Surface(
color = Color.Green,
modifier = Modifier
.padding(horizontal = 30.dp)
.size(40.dp)
.graphicsLayer {
rotationZ = zRotationAnimation1.value
}
) {}
Surface(
color = Color.Green,
modifier = Modifier
.padding(horizontal = 30.dp)
.size(40.dp)
.graphicsLayer {
rotationZ = zRotationAnimation2.value
}
) {}
}
}
AnimatedVisibility 隱藏顯示動畫
默認效果
效果圖
代碼
@Composable
fun APage() {
val imageVisible = remember {
mutableStateOf(true)
}
//這邊用一個協程,以1500毫秒反覆顯示與隱藏
LaunchedEffect(true){
for (i in 0..10){
delay(1500)
imageVisible.value = !imageVisible.value
}
}
Box(modifier = Modifier.fillMaxSize()) {
AnimatedVisibility(visible = imageVisible.value, modifier = Modifier.align(Alignment.Center)) {
Image(
painter = painterResource(id = R.mipmap.ic_fruit_apple),
contentDescription = null,
modifier = Modifier
.size(100.dp)
)
}
}
}
淡入淡出效果 fadeIn與fadeOut
效果圖
代碼
@Composable
fun APage() {
val imageVisible = remember {
mutableStateOf(true)
}
//這邊用一個協程,以1500毫秒反覆顯示與隱藏
LaunchedEffect(true) {
for (i in 0..50) {
delay(1500)
imageVisible.value = !imageVisible.value
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
//>>>>>>>>>淡入淡出效果 fadeIn與fadeOut
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = imageVisible.value,
enter = fadeIn(
initialAlpha = 0f
),
exit = fadeOut(targetAlpha = 0f)
) {
Image(
painter = painterResource(id = R.mipmap.ic_fruit_watermelon),
contentDescription = null,
modifier = Modifier
.size(100.dp)
)
}
}
}
滑入滑出 slideIn與slideOut
效果圖
代碼
@Composable
fun APage() {
val imageVisible = remember {
mutableStateOf(true)
}
//這邊用一個協程,以1500毫秒反覆顯示與隱藏
LaunchedEffect(true) {
for (i in 0..50) {
delay(1500)
imageVisible.value = !imageVisible.value
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
//>>>>>>>>>滑入滑出 slideIn與slideOut
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = imageVisible.value,
enter = slideIn { IntOffset(-100, -100) },
exit = slideOut { IntOffset(100, 100) }
) {
Image(
painter = painterResource(id = R.mipmap.ic_fruit_watermelon),
contentDescription = null,
modifier = Modifier
.size(100.dp)
)
}
}
}
水平滑入與水平滑出 slideInHorizontally與slideOutHorizontally
效果圖
代碼
@Composable
fun APage() {
val imageVisible = remember {
mutableStateOf(true)
}
//這邊用一個協程,以1500毫秒反覆顯示與隱藏
LaunchedEffect(true) {
for (i in 0..50) {
delay(1500)
imageVisible.value = !imageVisible.value
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
//>>>>>>>>>水平滑入與水平滑出 slideInHorizontally與slideOutHorizontally
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = imageVisible.value,
enter = slideInHorizontally(initialOffsetX = {fullWidth->
return@slideInHorizontally -(fullWidth/2)
}),
exit = slideOutHorizontally(targetOffsetX = {fullWidth->
return@slideOutHorizontally fullWidth/2
})
) {
Image(
painter = painterResource(id = R.mipmap.ic_fruit_watermelon),
contentDescription = null,
modifier = Modifier
.size(100.dp)
)
}
}
}
垂直滑入與垂直滑出 slideInVertically與slideOutVertically
效果圖
代碼
@Composable
fun APage() {
val imageVisible = remember {
mutableStateOf(true)
}
//這邊用一個協程,以1500毫秒反覆顯示與隱藏
LaunchedEffect(true) {
for (i in 0..50) {
delay(1500)
imageVisible.value = !imageVisible.value
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
//>>>>>>>>>垂直滑入與垂直滑出 slideInVertically與slideOutVertically
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = imageVisible.value,
enter = slideInVertically(initialOffsetY = {fullHeight->
return@slideInVertically -(fullHeight/2)
}),
exit = slideOutVertically(targetOffsetY = {fullHeight->
return@slideOutVertically fullHeight/2
})
) {
Image(
painter = painterResource(id = R.mipmap.ic_fruit_watermelon),
contentDescription = null,
modifier = Modifier
.size(100.dp)
)
}
}
}
縮放進入與縮放退出 scaleIn與scaleOut
效果圖
代碼
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun APage() {
val imageVisible = remember {
mutableStateOf(true)
}
//這邊用一個協程,以1500毫秒反覆顯示與隱藏
LaunchedEffect(true) {
for (i in 0..50) {
delay(1500)
imageVisible.value = !imageVisible.value
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
//>>>>>>>>>縮放進入與縮放退出 scaleIn與scaleOut
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = imageVisible.value,
enter = scaleIn(animationSpec = spring(),initialScale = 0.1f, transformOrigin = TransformOrigin.Center),
exit = scaleOut(animationSpec = spring(),targetScale = 0.1f, transformOrigin = TransformOrigin.Center)
) {
Image(
painter = painterResource(id = R.mipmap.ic_fruit_watermelon),
contentDescription = null,
modifier = Modifier
.size(100.dp)
)
}
}
}
擴展與收縮 expandIn與shrinkOut
效果圖
代碼
@Composable
fun APage() {
val imageVisible = remember {
mutableStateOf(true)
}
//這邊用一個協程,以1500毫秒反覆顯示與隱藏
LaunchedEffect(true) {
for (i in 0..50) {
delay(1500)
imageVisible.value = !imageVisible.value
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
//>>>>>>>>>擴展與收縮 expandIn與shrinkOut
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = imageVisible.value,
enter = expandIn(),
exit = shrinkOut()
) {
Image(
painter = painterResource(id = R.mipmap.ic_fruit_watermelon),
contentDescription = null,
modifier = Modifier
.size(100.dp)
)
}
}
}
水平擴展與水平收縮 expandHorizontally與shrinkHorizontally
效果圖
代碼
@Composable
fun APage() {
val imageVisible = remember {
mutableStateOf(true)
}
//這邊用一個協程,以1500毫秒反覆顯示與隱藏
LaunchedEffect(true) {
for (i in 0..50) {
delay(1500)
imageVisible.value = !imageVisible.value
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
//>>>>>>>>>水平擴展與水平收縮 expandHorizontally與shrinkHorizontally
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = imageVisible.value,
enter = expandHorizontally(),
exit = shrinkHorizontally()
) {
Image(
painter = painterResource(id = R.mipmap.ic_fruit_watermelon),
contentDescription = null,
modifier = Modifier
.size(100.dp)
)
}
}
}
垂直擴展與垂直收縮 expandVertically與shrinkVertically
效果圖
代碼
@Composable
fun APage() {
val imageVisible = remember {
mutableStateOf(true)
}
//這邊用一個協程,以1500毫秒反覆顯示與隱藏
LaunchedEffect(true) {
for (i in 0..50) {
delay(1500)
imageVisible.value = !imageVisible.value
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
//>>>>>>>>>垂直擴展與垂直收縮 expandVertically與shrinkVertically
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = imageVisible.value,
enter = expandVertically(),
exit = shrinkVertically()
) {
Image(
painter = painterResource(id = R.mipmap.ic_fruit_watermelon),
contentDescription = null,
modifier = Modifier
.size(100.dp)
)
}
}
}
屬性動畫
一共有如下類型值的屬性動畫函數,他們的使用都是類似的。此外這些屬性動畫函數還需要配合插值器使用。
- animateValueAsState : 其他的animateXxxAsState內部都是調用的這個
- animateRectAsState : 參數是傳的一個Rect對象,Rect(left,top,right,bottom)
- animateIntAsState : 參數傳的是Int
- animateDpAsState : 參數傳的是Dp
- animateFloatAsState : 參數傳的是Float
- animateColorAsState : 參數傳的是Color
- animateOffsetAsState : 參數傳的是Offset,Offset(x,y),x和y是Float類型
- animateIntOffsetAsState : 參數傳的是IntOffset,IntOffset(x,y),x和y是Int類型
- animateSizeAsState : 參數傳的是Size,Size(width,height),width和height是Float類型
- animateIntSizeAsState : 參數傳的是IntSize,IntSize(width,height),width和height是Int類型
animateFloatAsState與spring插值器
上面的屬性動畫函數使用都是類似的,這裏以animateFloatAsState與spring插值器來舉例
y軸移動動畫
效果圖
代碼
@Composable
fun APage() {
val startAnim = remember {
mutableStateOf(false)
}
//創建y軸的動畫數值
val yAnimValue = animateDpAsState(
//targetValue爲動畫的目標數值
targetValue = if (startAnim.value) 200.dp else 0.dp,
//animationSpec動畫的可選插值器,目前的結尾彈跳效果就是spring這個插值器實現的
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
)
)
Box(
modifier = Modifier.fillMaxSize()
) {
Surface(color = Color.Gray, modifier = Modifier
.graphicsLayer {
translationY = yAnimValue.value.toPx()
}
.size(50.dp)
.align(Alignment.Center)
.clickable {
//點擊啓動動畫
startAnim.value = !startAnim.value
}) {
}
}
}
tween插值器
tween插值器可以設置動畫時間
效果圖
代碼
@Composable
fun Animation() {
val position = remember {
mutableStateOf(Offset(0f, 0f))
}
val duration = remember {
mutableStateOf(3000)
}
//動畫數值
val animValue = animateOffsetAsState(
targetValue = position.value,
animationSpec = tween(durationMillis = duration.value, easing = LinearEasing)
)
//移動位置集合
val starList = listOf(
//第一個參數是座標,第二個參數延遲時間
Offset(-100f, -100f) to 1000,//左上
Offset(-100f, 100f) to 1000,//左下
Offset(100f, 100f) to 1000,//右下
Offset(100f, -100f) to 1000,//右上
)
LaunchedEffect(true) {
delay(2000)
for (item in starList) {
position.value = item.first
duration.value = item.second
delay(duration.value.toLong())
}
}
Box(modifier = Modifier.fillMaxSize()) {
Image(
painter = painterResource(id = R.mipmap.ic_red_dot),
contentDescription = null,
modifier = Modifier
.graphicsLayer {
translationX = animValue.value.x
translationY = animValue.value.y
}
.align(Alignment.Center)
)
}
}
在Canvas裏的使用例子
效果圖
代碼
@Composable
fun AnimationBg() {
//-400.dp 到 -1500.dp 動畫範圍值
val yPosition = remember {
mutableStateOf(-400.dp)
}
//y軸動畫數值
val yAnimValue = animateDpAsState(
targetValue = yPosition.value,
animationSpec = tween(durationMillis = 15_000, easing = LinearEasing)
)
val rotate = remember {
mutableStateOf(0f)
}
//旋轉動畫,請注意這裏使用的是animateFloatAsState
val rotateAnimValue = animateFloatAsState(
targetValue = rotate.value,
animationSpec = tween(durationMillis = 5000, easing = LinearEasing)
)
LaunchedEffect(true) {
var isFront = true
val angleList = listOf<Float>(0f, 45f, 90f, 135f)
var count = 0
while (isActive) {
isFront = !isFront
if (isFront) {
yPosition.value = -400.dp
} else {
yPosition.value = -1500.dp
}
delay(15_000)
count++
if (count > 2){
count = 0
rotate.value = angleList.random()
}
}
}
Canvas(
modifier = Modifier
.fillMaxSize()
.rotate(rotateAnimValue.value)
) {
val itemHeight = size.height / 30
for (index in 0..200) {
if (index % 2 == 0) {
//黑色
drawLine(
color = Color.Black,
start = Offset(-500f, itemHeight * index + yAnimValue.value.toPx()),
end = Offset(size.width + 500f, itemHeight * index + yAnimValue.value.toPx()),
strokeWidth = itemHeight
)
} else {
//白色
drawLine(
color = Color.White,
start = Offset(-500f, itemHeight * index + yAnimValue.value.toPx()),
end = Offset(size.width + 500f, itemHeight * index + yAnimValue.value.toPx()),
strokeWidth = itemHeight
)
}
}
}
}
end