iT邦幫忙

2021 iThome 鐵人賽

DAY 13
0
Mobile Development

認真學 Compose - 對 Jetpack Compose 的問題與探索系列 第 13

D13/ 怎麼做翻卡片的動畫 - Animation Part 2 & GraphicsLayer

今天大概會聊到的範圍

  • Animation
  • Modifier.graphicsLayer

上一篇講到 Animation,今天想要拿 Animation 來實作看看。

再跑敏捷的過程中,常常需要舉牌投點。我都會用 Scrum Time 這個 App 來進行。在這個 App 中,點數牌打開時有一個漂亮的翻牌動畫。今天,我想用 Compose Animation 來試圖達到一樣的效果。

先做出牌面

在開始之前,我先做出基本的牌面

@Composable
fun CardBack() {
    Card(
        modifier = Modifier
            .aspectRatio(.65f)
            .defaultMinSize(minHeight = 60.dp),
        backgroundColor = Color.Blue,
        border = BorderStroke(width = 16.dp, color = Color.White)
    ) {
    
    }
}


@Composable
fun CardFront() {
    Card(
        contentColor = SpotiColor.Black,
        backgroundColor = Color.coverColor3,
        border = BorderStroke(width = 16.dp, color = Color.White),
        modifier = Modifier
            .aspectRatio(.65f)
            .defaultMinSize(minHeight = 60.dp)
    ) {
        Box(modifier = Modifier.fillMaxSize()) {
            Text(
                text = "Front",
                fontSize = 36.sp,
                fontWeight = FontWeight.Black,
                color = Color.White,
                textAlign = TextAlign.Center,
                modifier = Modifier.align(Alignment.Center)
            )
        }
    }
}
CardFront() CardBack()

讓牌視覺上有翻轉效果

在動畫之前,我想先瞭解如何讓我的卡片“看起來”像是有翻轉。

先說到旋轉,最常見的旋轉是繞著 Z 軸(垂直於螢幕平面的軸)旋轉。我們可以透過 Modifier.rotate 來修飾元件達到這件事情。

@Preview
@Composable
fun PreviewCardFront() {
    Box(modifier = Modifier.rotate(30f)) {
        CardFront()
    }
}

rotate 內部的實作,其實是透過另一個 Modifier graphicsLayer

fun Modifier.rotate(degrees: Float) =
    if (degrees != 0f) graphicsLayer(rotationZ = degrees) else this

graphicsLayer 這個 modifier 可以對他修飾的 component 進行圖形上的改動。例如放大縮小、旋轉、形狀與透明度。

fun Modifier.graphicsLayer(
    // 放大縮小
    scaleX: Float = 1f,
    scaleY: Float = 1f,
    // 透明度
    alpha: Float = 1f,
    // 位移
    translationX: Float = 0f,
    translationY: Float = 0f,
    // 陰影
    shadowElevation: Float = 0f,
    // 繞著不同軸旋轉
    rotationX: Float = 0f,
    rotationY: Float = 0f,
    rotationZ: Float = 0f,
    cameraDistance: Float = DefaultCameraDistance,
    transformOrigin: TransformOrigin = TransformOrigin.Center,
    shape: Shape = RectangleShape,
    clip: Boolean = false,
    renderEffect: RenderEffect? = null
): Modifier

除了 rotate 外,Modifier.clipModifier.alphaModifier.scale 等 modifier 也都是對 graphicsLayer 的包裝。
回到這次的目標,我們需要繞著 Y 軸選轉我們的牌。因此,我們透過 graphicsLayer 並且設定 rotationY

@Preview
@Composable
fun PreviewCardFront() {
    Box(modifier = Modifier.graphicsLayer(rotationY = 30f)) {
        CardFront()
    }
}

https://ithelp.ithome.com.tw/upload/images/20210927/20141597Zf0XOynzTD.png

知道卡片該怎麼轉之後,就可以開始來做動畫了。

開始建構畫面

// 背景
Box(
    modifier = Modifier
        .fillMaxSize()
        .background(color = darkBgColor)
) {
    // Card 包裝
    Box(
        modifier = Modifier
            .fillMaxSize(.6f)
            .align(Alignment.Center)
    ) {
        CardFront()
    }
}

我希望在點擊卡片後,卡片就會翻轉。因此,在卡片上 ( Box 那一層 ) 增加一個 clickable 的 modifier。點擊後,修改目前應該要是正面或反面的 state。

enum class CardState { Front, Back }


// in Composable
var state by remember { mutableStateOf(CardState.Front) }

// 背景
Box(
    modifier = Modifier
        .fillMaxSize()
        .background(color = darkBgColor)
) {
    // Card 包裝
    Box(
        modifier = Modifier
            .fillMaxSize(.6f)
            .align(Alignment.Center)
            .clickable {
                state = when (state) {
                    CardState.Front -> CardState.Back
                    CardState.Back -> CardState.Front
                }
            }
    ) {
        CardFront()
    }
}

開始做動畫

上次有提到,我們可以用 updateTransition 來將 state 轉成 transition。可以透過同一個 transition 來控制不同動態的值。

var state by remember { mutableStateOf(CardState.Front) }

val flipTransition = updateTransition(targetState = state)

// 正面時不翻轉,反面時翻轉 180 度
val rotateY by flipTransition.animateFloat {
    when (it) {
        CardState.Front -> 0f
        CardState.Back -> 180f
    }
}

// 背景
Box(
    modifier = Modifier
        .fillMaxSize()
        .background(color = darkBgColor)
) {
    // Card 包裝
    Box(
        modifier = Modifier
            .fillMaxSize(.75f)
            .align(Alignment.Center)
            .clickable {
                state = when (state) {
                    CardState.Front -> CardState.Back
                    CardState.Back -> CardState.Front
                }
            }
            .graphicsLayer {
                rotationY = rotateY       // <-- 使用 rotateY 這個參數當作 rotationY 的值
            }
    
    ) {
        CardFront()
    }
}

在翻轉時,會發現卡的邊邊會被削掉。因為虛擬的 "攝影機" 和實際元件的距離短於卡片寬度的一半,導致卡片翻轉時,卡片的邊會超過虛擬攝影機的鏡頭。

當我們要做 rotationY / rotationX 時,都建議在 graphicsLayer 加上 cameraDistance,設定一個大於卡片寬度的距離。

.graphicsLayer {
    rotationY = rotateY
    cameraDistance = DefaultCameraDistance * density
}

因為要翻牌,我們希望卡片在翻到 90 度的時候,顯示的內容由卡面換成卡背。這個部分我一樣可以透過 ratateY 這個值來做判斷

if (rotateY <= 90f) {
    CardFront()
} else {
    CardBack()
}

優化動畫

到目前為止,翻轉卡片的動畫已經大致上完成。但是還想要改變幾個部分:

  • 反轉的速度太快了
  • 希望和 Scrum Timer 的效果類似,在翻轉時稍微縮小卡片

在 transition 轉成 animation 時,或是 animateXXXAsState 等 function 時,都可以加入 transitionSpectransitionSpec 可以調整動畫的影格對應到參數的變動速率。

val rotateY by flipTransition.animateFloat(
    transitionSpec = {
        tween(
            delayMillis = 50,
            durationMillis = 500,
            easing = LinearOutSlowInEasing
        )
    }
) {
    when (it) {
        CardState.Front -> 0f
        CardState.Back -> 180f
    }
}

常用的 spec 有:

  • spring:數值 A 到數值 B 的曲線會類似彈簧一樣(可以設定彈力係數和力道),當力道強的時候會來回彈跳
  • tween:可以設定數值 A 到 B 的時間,並且透過 easing 設定數值變動的曲線
  • keyframes:設定每個關鍵影格所代表的值(以 ms 為單位)
  • repeatable:數值會在 A B 之間不斷重複,到某個固定的值。還有 infiniteRepeatable 可以無限重複
  • snap:會瞬間將數值 A 轉變成數值 B

透過一樣的概念,我們可以透過 transition 獨立出另一個數值 scale。並且透過 keyframe 達到讓 Scale 進行 大 > 小 > 大 的動畫。

val scale by flipTransition.animateFloat(
    transitionSpec = {
        keyframes {
            durationMillis = 500
            .6f at 250 with LinearEasing
        }
    }
) {
    when (it) {
        CardState.Front -> 1f
        CardState.Back -> 1f
    }
}

keyframe 中,可以透過 <數值> at <影格 (ms)> with <easing> 來設定影格。start 會動畫啟動當下的值,end 會是 targetValue ( 後面提供的 lambda 所提供的值 )

.graphicsLayer {
    rotationY = rotateY
    cameraDistance = DefaultCameraDistance * density
    scaleX = scale
    scaleY = scale
}

最後,在 graphicsLayer 可以設定 scaleXscaleY 依照 scale 這個值縮放


卡片翻轉的動畫就這樣完成了!透過實作範例比較能了解複雜的 Animation 中各種工具的參數。今天的範例還是有很多部分沒有接觸到,未來也許可以再回頭來看看。


Reference:


上一篇
D12/ 我要怎麼用動畫改變中的資料? - Animations
下一篇
D14/ 怎麼做拉動的操作? - Draggable Gesture
系列文
認真學 Compose - 對 Jetpack Compose 的問題與探索30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言