今天大概會聊到的範圍
- 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.clip
、Modifier.alpha
和 Modifier.scale
等 modifier 也都是對 graphicsLayer
的包裝。
回到這次的目標,我們需要繞著 Y 軸選轉我們的牌。因此,我們透過 graphicsLayer
並且設定 rotationY
。
@Preview
@Composable
fun PreviewCardFront() {
Box(modifier = Modifier.graphicsLayer(rotationY = 30f)) {
CardFront()
}
}
知道卡片該怎麼轉之後,就可以開始來做動畫了。
// 背景
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()
}
到目前為止,翻轉卡片的動畫已經大致上完成。但是還想要改變幾個部分:
在 transition 轉成 animation 時,或是 animateXXXAsState
等 function 時,都可以加入 transitionSpec
,transitionSpec
可以調整動畫的影格對應到參數的變動速率。
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
可以設定 scaleX
、scaleY
依照 scale
這個值縮放
卡片翻轉的動畫就這樣完成了!透過實作範例比較能了解複雜的 Animation 中各種工具的參數。今天的範例還是有很多部分沒有接觸到,未來也許可以再回頭來看看。
Reference: