動畫修飾符和可組合函式

Compose 內建可組合項和修飾符,可處理常見的動畫用途。

內建動畫可組合函式

Compose 提供多個可組合函式,可為內容的顯示、消失和版面配置變更加入動畫效果。

以動畫方式呈現顯示與消失

綠色可組合函式會顯示及隱藏自身
圖 1. 以動畫方式呈現資料欄中項目的顯示與消失。

AnimatedVisibility 可組合項以動畫方式呈現內容的顯示與消失。

var visible by remember {
    mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(visible) {
    // your composable here
    // ...
}

根據預設,內容會以淡入與展開的效果顯示,並且以淡出和縮小方式消失。只要指定 EnterTransitionExitTransition 物件,即可自訂這個轉場效果。

var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
AnimatedVisibility(
    visible = visible,
    enter = slideInVertically {
        // Slide in from 40 dp from the top.
        with(density) { -40.dp.roundToPx() }
    } + expandVertically(
        // Expand from the top.
        expandFrom = Alignment.Top
    ) + fadeIn(
        // Fade in with the initial alpha of 0.3f.
        initialAlpha = 0.3f
    ),
    exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
    Text(
        "Hello",
        Modifier
            .fillMaxWidth()
            .height(200.dp)
    )
}

如上例所示,您可以將多個 EnterTransitionExitTransition 物件與 + 運算子合併,並為每個物件納入選用參數來自訂行為。詳情請參閱參考頁面。

進入和離開轉換動畫範例

EnterTransition ExitTransition
fadeIn
UI 元素會逐漸淡入畫面。
fadeOut
UI 元素逐漸從檢視畫面淡出。
slideIn
UI 元素從螢幕外滑入畫面。
slideOut
UI 元素從螢幕上滑出畫面。
slideInHorizontally
UI 元素會以水平方式滑入檢視畫面。
slideOutHorizontally
UI 元素會從檢視畫面水平滑出。
slideInVertically
UI 元素會以垂直方式滑入檢視畫面。
slideOutVertically
UI 元素會垂直滑出畫面。
scaleIn
UI 元素會放大並顯示在畫面上。
scaleOut
UI 元素縮小並移出畫面。
expandIn
UI 元素從中心點展開並進入檢視畫面。
shrinkOut
UI 元素會從檢視畫面縮小至中心點。
expandHorizontally
UI 元素會水平展開並顯示在檢視區塊中。
shrinkHorizontally
UI 元素在水平方向縮小,超出檢視畫面。
expandVertically
UI 元素會以垂直方式展開並進入畫面。
shrinkVertically
UI 元素會垂直縮小,並移出畫面。

AnimatedVisibility 也提供可包含 MutableTransitionState 引數的變化版本,可讓您在 AnimatedVisibility 可組合函式新增至組合樹時立即觸發動畫。這項元件也有助於觀察動畫狀態。

// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.
val state = remember {
    MutableTransitionState(false).apply {
        // Start the animation immediately.
        targetState = true
    }
}
Column {
    AnimatedVisibility(visibleState = state) {
        Text(text = "Hello, world!")
    }

    // Use the MutableTransitionState to know the current animation state
    // of the AnimatedVisibility.
    Text(
        text = when {
            state.isIdle && state.currentState -> "Visible"
            !state.isIdle && state.currentState -> "Disappearing"
            state.isIdle && !state.currentState -> "Invisible"
            else -> "Appearing"
        }
    )
}

為子項建立進入與結束動畫效果

AnimatedVisibility (直接或間接子項) 中的內容可使用 animateEnterExit 輔助鍵為每個子項指定不同的動畫行為。每個子項的視覺效果都是 AnimatedVisibility 可組合項中指定的動畫和子項本身的進入和結束動畫組合而成。

var visible by remember { mutableStateOf(true) }

AnimatedVisibility(
    visible = visible,
    enter = fadeIn(),
    exit = fadeOut()
) {
    // Fade in/out the background and the foreground.
    Box(
        Modifier
            .fillMaxSize()
            .background(Color.DarkGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .animateEnterExit(
                    // Slide in/out the inner box.
                    enter = slideInVertically(),
                    exit = slideOutVertically()
                )
                .sizeIn(minWidth = 256.dp, minHeight = 64.dp)
                .background(Color.Red)
        ) {
            // Content of the notification…
        }
    }
}

在某些情況下,您可能希望 AnimatedVisibility 完全不套用任何動畫,即可透過 animateEnterExit 為子項個別指定不同的動畫。為此,請在 AnimatedVisibility 可組合項中指定 EnterTransition.NoneExitTransition.None

新增自訂動畫

除了內建的進入與結束動畫以外,如要新增自訂動畫效果,請使用 AnimatedVisibility 內容 lambda 中的 transition 屬性存取基礎 Transition 例項。新增至 Transition 執行個體的任何動畫狀態將會與 AnimatedVisibility 的進入與結束動畫同時執行。AnimatedVisibility 會等待 Transition 中的所有動畫都結束後,才會移除其中內容。如果是與 Transition 分開建立 (例如使用 animate*AsState) 的結束動畫,AnimatedVisibility 將無法將其納入考量,因此可以在其完成前移除內容可組合項。

var visible by remember { mutableStateOf(true) }

AnimatedVisibility(
    visible = visible,
    enter = fadeIn(),
    exit = fadeOut()
) { // this: AnimatedVisibilityScope
    // Use AnimatedVisibilityScope#transition to add a custom animation
    // to the AnimatedVisibility.
    val background by transition.animateColor(label = "color") { state ->
        if (state == EnterExitState.Visible) Color.Blue else Color.Gray
    }
    Box(
        modifier = Modifier
            .size(128.dp)
            .background(background)
    )
}

如要進一步瞭解如何使用 Transition 管理動畫,請參閱「使用轉場效果同時為多個屬性製作動畫」。

根據目標狀態加入動畫效果

AnimatedContent 可組合項可根據目標狀態產生變動時,為內容建立動畫效果。

Row {
    var count by remember { mutableIntStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Add")
    }
    AnimatedContent(
        targetState = count,
        label = "animated content"
    ) { targetCount ->
        // Make sure to use `targetCount`, not `count`.
        Text(text = "Count: $targetCount")
    }
}

根據預設,初始內容會淡出,然後目標內容淡入 (這個行為稱為淡出淡入切換)。只要將 ContentTransform 物件指定至 transitionSpec 參數,即可自訂這個動畫的行為。您可以使用 with infix 函式,結合 EnterTransition 物件與 ExitTransition 物件,建立 ContentTransform 的執行個體。您可以將 SizeTransform 套用至 ContentTransform 物件,做法是將它加至 using infix 函式裡。

AnimatedContent(
    targetState = count,
    transitionSpec = {
        // Compare the incoming number with the previous number.
        if (targetState > initialState) {
            // If the target number is larger, it slides up and fades in
            // while the initial (smaller) number slides up and fades out.
            slideInVertically { height -> height } + fadeIn() togetherWith
                slideOutVertically { height -> -height } + fadeOut()
        } else {
            // If the target number is smaller, it slides down and fades in
            // while the initial number slides down and fades out.
            slideInVertically { height -> -height } + fadeIn() togetherWith
                slideOutVertically { height -> height } + fadeOut()
        }.using(
            // Disable clipping since the faded slide-in/out should
            // be displayed out of bounds.
            SizeTransform(clip = false)
        )
    }, label = "animated content"
) { targetCount ->
    Text(text = "$targetCount")
}

EnterTransition 會定義目標內容的顯示方式,而 ExitTransition 則會定義初始內容的消失方式。除了可用於 AnimatedVisibility 的所有 EnterTransitionExitTransition 函式外,AnimatedContent 也提供 slideIntoContainerslideOutOfContainer。這些元件可做為 slideInHorizontally/VerticallyslideOutHorizontally/Vertically 簡便的替代項目,可根據初始內容的大小和 AnimatedContent 內容的目標內容來計算滑動距離。

SizeTransform 用於定義初始內容和目標內容之間動畫效果的大小。建立動畫時,您可以存取初始大小和目標大小,還能透過 SizeTransform 控制在動畫播放期間是否應該將內容裁剪為元件大小。

var expanded by remember { mutableStateOf(false) }
Surface(
    color = MaterialTheme.colorScheme.primary,
    onClick = { expanded = !expanded }
) {
    AnimatedContent(
        targetState = expanded,
        transitionSpec = {
            fadeIn(animationSpec = tween(150, 150)) togetherWith
                fadeOut(animationSpec = tween(150)) using
                SizeTransform { initialSize, targetSize ->
                    if (targetState) {
                        keyframes {
                            // Expand horizontally first.
                            IntSize(targetSize.width, initialSize.height) at 150
                            durationMillis = 300
                        }
                    } else {
                        keyframes {
                            // Shrink vertically first.
                            IntSize(initialSize.width, targetSize.height) at 150
                            durationMillis = 300
                        }
                    }
                }
        }, label = "size transform"
    ) { targetExpanded ->
        if (targetExpanded) {
            Expanded()
        } else {
            ContentIcon()
        }
    }
}

為子項進入和結束建立轉場動畫

AnimatedVisibility 一樣,AnimatedContent 的內容 lambda 中具有animateEnterExit 修飾元。使用此方法將 EnterAnimationExitAnimation 分別套用至每個直接或間接的子項中。

新增自訂動畫

AnimatedVisibility 一樣,transition 欄位可在 AnimatedContent 的內容 lambda 中使用。只要使用這個元件,即可建立可與 AnimatedContent 轉場同時執行的自訂動畫效果。詳情請參閱「updateTransition」。

在兩個版面配置之間建立動畫

Crossfade 可在兩個版面配置之間建立交叉漸變的動畫效果。透過切換傳遞至 current 參數的值,讓系統以交叉漸變的動畫效果切換顯示內容。

var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage, label = "cross fade") { screen ->
    when (screen) {
        "A" -> Text("Page A")
        "B" -> Text("Page B")
    }
}

內建動畫修飾符

Compose 提供修飾符,可直接在可組合函式上製作特定變更的動畫。

為可組合函式的大小變更加上動畫效果

綠色可組合函式,可順暢地為大小變化加上動畫效果。
圖 2. 可組合項,在較小和較大的尺寸之間順暢地製作動畫

animateContentSize 修飾詞可用於建立帶有大小變化的動畫效果。

var expanded by remember { mutableStateOf(false) }
Box(
    modifier = Modifier
        .background(colorBlue)
        .animateContentSize()
        .height(if (expanded) 400.dp else 200.dp)
        .fillMaxWidth()
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            expanded = !expanded
        }

) {
}

清單項目動畫

如果要在 Lazy 清單或格線中為項目重新排序建立動畫,請參閱 Lazy 版面配置項目動畫文件