Android開發 Jetpack Compose 動畫

前言

  此篇博客講解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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章