首頁 > 軟體

Compose 動畫藝術探索之可見性動畫範例詳解

2022-09-24 14:01:49

正文

本篇文章是此專欄的第二篇文章,上一篇文章簡單寫了下 Compose 的動畫,讓大家先看了下 Compose 開箱即用的動畫效果,效果還是挺好的,感興趣的可以去看下:Compose 動畫藝術探索之瞅下 Compose 的動畫

從可見性動畫看起

可見性動畫在上一篇文章中介紹過,不過只是簡單使用,沒看過上一篇文章的也不用擔心,給大家看下可見性動畫的實際效果。

實現程式碼也很簡單,來回顧下:

val visible = remember { mutableStateOf(true) }
AnimatedVisibility(visible = visible.value,) {
    Text(text = "天青色等煙雨,而我在等你,炊煙裊裊升起,隔江千萬裡")
}

上一篇文章主要介紹了 Compose 動畫的便攜之處,例如上面程式碼,確實非常簡單就能實現之前原生安卓中比較難實現的動畫效果,今天咱們就來稍微深入一點看看,從小節標題也能知道,就從可見性動畫來看!

怎麼看呢?直接點進去原始碼來看!先來看看 AnimatedVisibility 的函數定義吧!

@Composable
fun AnimatedVisibility(
    visible: Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandIn(),
    exit: ExitTransition = shrinkOut() + fadeOut(),
    label: String = "AnimatedVisibility",
    content: @Composable() AnimatedVisibilityScope.() -> Unit
) {
    val transition = updateTransition(visible, label)
    AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}

其實可組合項 AnimatedVisibility 不是隻有這一個,目前 Compose 1.3.0-beta02 版本中有六個,這個咱們待會再說,先看這一個,也是第一個,可以看到函數中一共可以接收六個引數,下面先來看看這六個引數分別有什麼作用吧:

  • visible:定義內容是否應該可見,true 為可見,false 為不可見
  • modifier:修飾符,這個就不多說了,在 ComposeModifier 簡直隨處可見
  • enter:內容出現時的動畫,可以看到這個引數有預設值,預設值為 fadeIn() + expandIn() ,大致意思為淡入並擴充套件開
  • exit:內容由可見變為不可見時的動畫,同樣的,這個引數也有預設值,預設值為 shrinkOut() + fadeOut() ,大致意思為縮小並淡出消失
  • label:字面意思理解為標籤,預設值為 AnimatedVisibility ,可以自定義做標記,用於區分不同動畫
  • content:需要新增可見性動畫的可組合項。

上面這些引數除了 enterexit 外都比較好理解,這裡就不做過多解釋,重點來看下 enterexit ,可以看到 enter 的型別為 EnterTransitionexit 的型別為 ExitTransition ,那麼接下來咱們來分別看看 EnterTransitionExitTransition 吧!

這裡其實有一個小問題,可以看到上面 Gif 圖中並不是淡入並擴充套件和縮小並消失,這是為什麼呢?繼續往下看就能找到答案!

進入過渡——EnterTransition

顧名思義,這個類主要是為了做進入過渡的,來簡單看下它的原始碼吧:

@Immutable
sealed class EnterTransition {
    internal abstract val data: TransitionData
    // 組合不同的進入轉換。組合的順序並不重要,因為這些將同時啟動
    @Stable
    operator fun plus(enter: EnterTransition): EnterTransition {
        return EnterTransitionImpl(
            TransitionData(
                fade = data.fade ?: enter.data.fade,
                slide = data.slide ?: enter.data.slide,
                changeSize = data.changeSize ?: enter.data.changeSize,
                scale = data.scale ?: enter.data.scale
            )
        )
    }
    companion object {
        // 當不需要輸入轉換時,可以使用此函數。
        val None: EnterTransition = EnterTransitionImpl(TransitionData())
    }
}

可以看到 EnterTransition 是一個密封類, 類中有一個抽象的不可變值 data ,型別為 TransitionData ,這個放到下面來說;類中還有一個函數,而且該函數有 operator 字首, 這表示運運算元過載,過載了“+”號,所以就可以使用“+”來組合不同的輸入動畫了,函數接收的引數也是 EnterTransition ,然後直接返回 EnterTransitionImpl ,又沒見過,怎麼辦?繼續看看 EnterTransitionImpl 是個啥!

@Immutable
private class EnterTransitionImpl(override val data: TransitionData) : EnterTransition()

可以看到 EnterTransitionImpl 類很簡單,是一個私有類,繼承自 EnterTransition ,注意類上有 Immutable 註解, Immutable 註解可用於將類標記為生成不可變範例,但類的不變性沒有得到驗證,它是型別的一種承諾,即在構造範例之後,所有公開可存取的屬性和欄位都不會更改。 EnterTransitionImpl 還需要實現父類別的抽象值,所有有 TransitionData 型別的引數 data ,上面咱們簡單提到了 TransitionData ,這裡來看下吧!

@Immutable
internal data class TransitionData(
    val fade: Fade? = null,
    val slide: Slide? = null,
    val changeSize: ChangeSize? = null,
    val scale: Scale? = null
)

可以看到 TransitionData 類也有 Immutable 註解,這裡就不做過多介紹,這是一個包內可見的資料類,裡面有四個不可變值,分別是 FadeSlideChangeSizeScale ,其實從名稱就能看出這幾個引數分別代表的意思,不過還是來看下它們的原始碼吧!

@Immutable
internal data class Fade(val alpha: Float, val animationSpec: FiniteAnimationSpec<Float>)
@Immutable
internal data class Slide(
    val slideOffset: (fullSize: IntSize) -> IntOffset,
    val animationSpec: FiniteAnimationSpec<IntOffset>
)
@Immutable
internal data class ChangeSize(
    val alignment: Alignment,
    val size: (fullSize: IntSize) -> IntSize = { IntSize(0, 0) },
    val animationSpec: FiniteAnimationSpec<IntSize>,
    val clip: Boolean = true
)
@Immutable
internal data class Scale(
    val scale: Float,
    val transformOrigin: TransformOrigin,
    val animationSpec: FiniteAnimationSpec<Float>
)

可以看到這四個類都是不可變的資料類,分別表示顏色轉變、滑動、大小變化及縮放。這幾個類有一個共同點,都有一個共同的引數 animationSpec ,引數型別為 FiniteAnimationSpec ,來看看 FiniteAnimationSpec 是個啥?

interface FiniteAnimationSpec<T> : AnimationSpec<T> {
    override fun <V : AnimationVector> vectorize(
        converter: TwoWayConverter<T, V>
    ): VectorizedFiniteAnimationSpec<V>
}

可以看到 FiniteAnimationSpec 是一個介面,繼承自 AnimationSpec ,簡單理解就是有限動畫規格,定義了動畫的時長及動畫效果等,類似於原生安卓中的什麼呢?嗯。。。差值器吧!FiniteAnimationSpec 是所有非無限動畫實現的介面,包括: TweenSpecSpringSpecKeyframesSpecRepeatableSpecSnapSpec 等等,上一篇文章中說到的無限迴圈動畫 InfiniteRepeatableSpec 沒有繼承這個介面,其實從名字看就知道了,InfiniteRepeatableSpec 也繼承自 AnimationSpec 。。。。

不行不行,扯太遠了,其實看原始碼就是這樣,看著看著一直往下看就會迷失了最初的目標,越看越多,越看越多,這裡就先不接著往下看了,再看就沒完沒了了,這裡咱們知道這四個類大概儲存了什麼資料就行了。動畫規格咱們放到之後的文章中慢慢看,今天主要來過一遍可見性動畫!

簡單總結下, EnterTransition 類中有一個函數,進行了運運算元過載,可以有多個動畫同時進行。

關閉過渡——ExitTransition

其實看完剛才的 EnterTransition 類再來看 ExitTransition 就會覺得很簡單了,不信的話咱們來看下 ExitTransition 的原始碼:

@Immutable
sealed class ExitTransition {
    internal abstract val data: TransitionData
    // 結合不同的退出轉換,組合順序並不重要
    @Stable
    operator fun plus(exit: ExitTransition): ExitTransition {
        return ExitTransitionImpl(
            TransitionData(
                fade = data.fade ?: exit.data.fade,
                slide = data.slide ?: exit.data.slide,
                changeSize = data.changeSize ?: exit.data.changeSize,
                scale = data.scale ?: exit.data.scale
            )
        )
    }
    companion object {
        // 當不需要內建動畫時使用
        val None: ExitTransition = ExitTransitionImpl(TransitionData())
    }
}

是不是基本一致,連抽象不可變值都一摸一樣,甚至值的名稱都沒變!唯一不同的是 EnterTransition 的實現類為 EnterTransitionImpl ,而 ExitTransition 的子類為 ExitTransitionImpl ,那就再看看 ExitTransitionImpl !

@Immutable
private class ExitTransitionImpl(override val data: TransitionData) : ExitTransition()

EnterTransitionImpl 不能說相似,只能說一摸一樣。。。所以就不做過多介紹。。。

過渡——Transition

文章開頭的時候看 AnimatedVisibility 函數中只有下面的兩行程式碼,

val transition = updateTransition(visible, label)
AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)

這一小節咱們先來看第一行,可以看到呼叫了 updateTransition 函數, 傳入了當前顯示狀態和標籤,來具體看下這個函數吧!

@Composable
fun <T> updateTransition(
    targetState: T,
    label: String? = null
): Transition<T> {
    val transition = remember { Transition(targetState, label = label) }
    transition.animateTo(targetState)
    DisposableEffect(transition) {
        onDispose {
            // 出去的時候清理乾淨,確保觀察者不會被困在中間狀態
            transition.onTransitionEnd()
        }
    }
    return transition
}

可以看到這個函數也是一個可組合項,返回值為 Transition ,函數中先記住並構建了一個 Transition ,然後呼叫了 TransitionanimateTo 函數來執行過渡動畫,然後呼叫了需要清理的效應 DisposableEffect ,之後在 onDispose 中呼叫了 onTransitionEnd 函數,以防卡在這裡,最後返回剛才構建的 Transition

函數中的內容不難理解,現在唯一困惑的是 Transition 是個什麼東西!那就來看看!

@Stable
class Transition<S> @PublishedApi internal constructor(
    private val transitionState: MutableTransitionState<S>,
    val label: String? = null
)

這就是 Transition 的類宣告,Transition 在狀態級別上管理所有子動畫。子動畫可以使用Transition 以宣告的方式建立。animateFloatanimateValueanimateColor animateColor等。當 targetState 改變時,Transition 將自動啟動或調整其所有子動畫的路線,使其動畫到為每個動畫定義的新目標值。

可以看到 Transition 建構函式中接收一個 MutableTransitionState 型別的引數和一個 lable ,但咱們看到上面傳入的並不是 MutableTransitionState 型別的引數,肯定還有別的建構函式!

internal constructor(
    initialState: S,
    label: String?
) : this(MutableTransitionState(initialState), label)

沒錯,確實還有一個建構函式,接下來看下 MutableTransitionState 吧!

class MutableTransitionState<S>(initialState: S) {
    // 當前的狀態
    var currentState: S by mutableStateOf(initialState)
        internal set
    // 過渡的目標狀態
    var targetState: S by mutableStateOf(initialState)
    // 是否空閒
    val isIdle: Boolean
        get() = (currentState == targetState) && !isRunning
    // 是否執行
    internal var isRunning: Boolean by mutableStateOf(false)
}

可以看到 MutableTransitionState 中構建了幾個需要的狀態,具體表示什麼在上面程式碼中新增了註釋。

過渡動畫實現——AnimatedEnterExitImpl

剛才看了 AnimatedVisibility 函數中的第一行程式碼,下面咱們來看下第二行程式碼:

AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)

這塊呼叫了一個實現函數,將剛構建好的 Transition 和之前的 EnterTransitionExitTransition 統統傳了進去,下面就來看下 AnimatedEnterExitImpl !

@Composable
private fun <T> AnimatedEnterExitImpl(
    transition: Transition<T>,visible: (T) -> Boolean,
    modifier: Modifier,enter: EnterTransition,
    exit: ExitTransition,content: @Composable() AnimatedVisibilityScope.() -> Unit
) {
    val isAnimationVisible = remember(transition) {
        mutableStateOf(visible(transition.currentState))
    }
    if (visible(transition.targetState) || isAnimationVisible.value || transition.isSeeking) {
        val childTransition = transition.createChildTransition(label = "EnterExitTransition") {
            transition.targetEnterExit(visible, it)
        }
        LaunchedEffect(childTransition) {
            snapshotFlow {
                childTransition.currentState == EnterExitState.Visible ||
                    childTransition.targetState == EnterExitState.Visible
            }.collect {
                isAnimationVisible.value = it
            }
        }
        AnimatedEnterExitImpl(childTransition,modifier,
            enter = enter,exit = exit,content = content
        )
    }
}

這塊程式碼有點多啊,但不要害怕,可以看到基本上這裡使用到的類在剛才咱們都看過了,接下來需要的就是一行一行往下看,看看哪個沒見過咱們再看!

函數中先記錄了當前的狀態,然後判斷當前的狀態值是否為 true,如果為 true 的話則建立子過渡,老規矩,來看看 createChildTransition

@Composable
inline fun <S, T> Transition<S>.createChildTransition(
    label: String = "ChildTransition",
    transformToChildState: @Composable (parentState: S) -> T,
): Transition<T> {
    val initialParentState = remember(this) { this.currentState }
    val initialState = transformToChildState(if (isSeeking) currentState else initialParentState)
    val targetState = transformToChildState(this.targetState)
    return createChildTransitionInternal(initialState, targetState, label)
}

可以看到 createChildTransitionTransition 的一個擴充套件函數,還是一個高階函數,函數內獲取了高階函數中的返回值,然後直接返回撥用了 createChildTransitionInternal ,並傳入獲取的值,這個值目前還不知道是什麼,只知道是 targetEnterExit 返回的資料,這個一會再看,先接著看 createChildTransitionInternal 函數:

@Composable
internal fun <S, T> Transition<S>.createChildTransitionInternal(
    initialState: T,
    targetState: T,
    childLabel: String,
): Transition<T> {
    val transition = remember(this) {
        Transition(MutableTransitionState(initialState), "${this.label} > $childLabel")
    }
    DisposableEffect(transition) {
        addTransition(transition)
        onDispose {
            removeTransition(transition)
        }
    }
    if (isSeeking) {
        transition.setPlaytimeAfterInitialAndTargetStateEstablished(
            initialState,
            targetState,
            this.lastSeekedTimeNanos
        )
    } else {
        transition.updateTarget(targetState)
        transition.isSeeking = false
    }
    return transition
}

可以看到 createChildTransitionInternal 也是 Transition 的一個擴充套件函數,然後函數中也使用了需要清理的效應,之後判斷 isSeeking 的值是,isSeeking 的值在 setPlaytimeAfterInitialAndTargetStateEstablished 函數中會被置為 true,如果 isSeeking 值為 true 則呼叫 setPlaytimeAfterInitialAndTargetStateEstablished 函數,用來設定初始狀態和目標狀態建立後的時間,如果為 false,則更新狀態值。

到這裡 createChildTransition 函數咱們大概看了下,但剛才還有一個扣,剛才說了不知道目標值是什麼,因為那是 targetEnterExit 的返回值,現在咱們來看看!

@Composable
private fun <T> Transition<T>.targetEnterExit(
    visible: (T) -> Boolean,
    targetState: T
): EnterExitState = key(this) {
    if (this.isSeeking) {
        if (visible(targetState)) {
            Visible
        } else {
            if (visible(this.currentState)) {
                PostExit
            } else {
                PreEnter
            }
        }
    } else {
        val hasBeenVisible = remember { mutableStateOf(false) }
        if (visible(currentState)) {
            hasBeenVisible.value = true
        }
        if (visible(targetState)) {
            EnterExitState.Visible
        } else {
            // If never been visible, visible = false means PreEnter, otherwise PostExit
            if (hasBeenVisible.value) {
                EnterExitState.PostExit
            } else {
                EnterExitState.PreEnter
            }
        }
    }
}

同樣的,targetEnterExit 也是一個擴充套件函數,返回值為 EnterExitState 。這裡也使用了 isSeeking 來進行判斷,然後根據當前的值來設定不同的 EnterExitStateEnterExitState 又是個啥呢???接著來看!

enum class EnterExitState {
    // 自定義進入動畫的初始狀態。
    PreEnter,
    // 自定義進入動畫的目標狀態,也是動畫過程中自定義退出動畫的初始狀態。
    Visible,
    // 自定義退出動畫的目標狀態。
    PostExit
}

奧,EnterExitState 只是一個列舉類,定義了三種狀態:初始狀態、進入動畫的狀態和退出動畫的狀態。

下面接著來看 AnimatedEnterExitImpl

LaunchedEffect(childTransition) {
            snapshotFlow {
                childTransition.currentState == EnterExitState.Visible ||
                    childTransition.targetState == EnterExitState.Visible
            }.collect {
                isAnimationVisible.value = it
            }
        }
AnimatedEnterExitImpl(childTransition,modifier,
            enter = enter,exit = exit,content = content
        )

這裡使用了 LaunchedEffect 效應,並使用 snapshotFlowState 轉為了 Flow ,然後將值設定到 isAnimationVisible

後面又呼叫了相同名字的一個函數 AnimatedEnterExitImpl

@Composable
private inline fun AnimatedEnterExitImpl(
    transition: Transition<EnterExitState>,
    modifier: Modifier,
    enter: EnterTransition,
    exit: ExitTransition,
    content: @Composable AnimatedVisibilityScope.() -> Unit
) {
    if (transition.currentState == EnterExitState.Visible ||
        transition.targetState == EnterExitState.Visible
    ) {
        val scope = remember(transition) { AnimatedVisibilityScopeImpl(transition) }
        Layout(
            content = { scope.content() },
            modifier = modifier.then(transition.createModifier(enter, exit, "Built-in")),
            measurePolicy = remember { AnimatedEnterExitMeasurePolicy(scope) }
        )
    }
}

函數名字一樣,但引數不同,這裡將剛才構建好的 Transition<EnterExitState> 傳了進來,然後函數內先對狀態進行了過濾,然後構建了 Layout

Layout 中設定了 modifiermodifier 呼叫了 then 函數,then 函數用於將此修飾符與另一個修飾符連線,然後連線了 transition.createModifier(enter, exit, "Built-in") ,大家發現點什麼沒有,這裡使用到了上面咱們構建好了所有東西。。。那麼。。。。哈哈哈哈!

柳暗花明

下面咱們就來看看 transition.createModifier 這個函數,先來看下函數體:

@Composable
internal fun Transition<EnterExitState>.createModifier(
    enter: EnterTransition,
    exit: ExitTransition,
    label: String
): Modifier

嗯,沒錯,是 Transition<EnterExitState> 的一個擴充套件函數,也是一個可組合項!接著往下看幾行程式碼:

var modifier: Modifier = Modifier
modifier = modifier.slideInOut(
    this,
    rememberUpdatedState(enter.data.slide),
    rememberUpdatedState(exit.data.slide),
    label
).shrinkExpand(
    this,
    rememberUpdatedState(enter.data.changeSize),
    rememberUpdatedState(exit.data.changeSize),
    label
)

構建了一個 Modifier ,然後呼叫了 slideInOutshrinkExpand ,沒錯,這是滑動和縮小放大!接著來!

var shouldAnimateAlpha by remember(this) { mutableStateOf(false) }
var shouldAnimateScale by remember(this) { mutableStateOf(false) }
if (currentState == targetState && !isSeeking) {
    shouldAnimateAlpha = false
    shouldAnimateScale = false
} else {
    if (enter.data.fade != null || exit.data.fade != null) {
        shouldAnimateAlpha = true
    }
    if (enter.data.scale != null || exit.data.scale != null) {
        shouldAnimateScale = true
    }
}

建立兩個值來記錄是否需要透明度的轉換和縮放!下面來看下執行的程式碼:

if (shouldAnimateScale) {
    ......
    modifier = modifier.graphicsLayer {
        this.alpha = alpha
        this.scaleX = scale
        this.scaleY = scale
        this.transformOrigin = transformOrigin
    }
} else if (shouldAnimateAlpha) {
    modifier = modifier.graphicsLayer {
        this.alpha = alpha
    }
}

嗯呢,是不是豁然開朗!但是 slideInOutshrinkExpand 函數也是可見性動畫這裡定義的 Modifier 的擴充套件函數,裡面還有一些自定義的東西,但這不是本文的重點了。

別的可見性動畫

文章開頭說了,可見性動畫目前一共有六個,聽著很嚇人,其實大同小異,來簡單看下不同吧!

@Composable
fun RowScope.AnimatedVisibility(
    visible: Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandHorizontally(),
    exit: ExitTransition = fadeOut() + shrinkHorizontally(),
    label: String = "AnimatedVisibility",
    content: @Composable() AnimatedVisibilityScope.() -> Unit
) {
    val transition = updateTransition(visible, label)
    AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}
@Composable
fun ColumnScope.AnimatedVisibility(
    visible: Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandVertically(),
    exit: ExitTransition = fadeOut() + shrinkVertically(),
    label: String = "AnimatedVisibility",
    content: @Composable AnimatedVisibilityScope.() -> Unit
) {
    val transition = updateTransition(visible, label)
    AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}

上面這兩個和咱們上面說的基本一樣,只不過一個是 RowScope 的擴充套件函數,另一個是 ColumnScope 的擴充套件函數,並且在預設動畫上還有些區別,RowScope 預設是橫向的擴充套件和收縮,ColumnScope 是縱向的擴充套件和收縮,更加方便咱們日常呼叫!這也就解釋了文章開頭提出的小問題!

接著再來看別的!

@Composable
fun AnimatedVisibility(
    visibleState: MutableTransitionState<Boolean>,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandIn(),
    exit: ExitTransition = fadeOut() + shrinkOut(),
    label: String = "AnimatedVisibility",
    content: @Composable() AnimatedVisibilityScope.() -> Unit
) {
    val transition = updateTransition(visibleState, label)
    AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}

這個和之前的就有點區別了,第一個引數就不同了,引數型別為 MutableTransitionState ,其實是一樣的,咱們上面也都說到了,MutableTransitionState 的使用方法在上一篇文章中也介紹過,感興趣的可以去看一下。

剩下的兩個還是 RowScopeColumnScope 的擴充套件函數,也是引數型別改為了 MutableTransitionState ,這裡也就不做過多介紹。

結尾

本篇文章帶大家看了可見性動畫的一些原始碼,很多其實都是點到為止,並沒有一致不斷深入,一直深入就會陷入其中,忘了看原始碼的本意,本文所有原始碼基於 Compose 1.3.0-beta02

本文至此結束,有用的地方大家可以參考,當然如果能幫助到大家,更多關於Compose 可見性動畫的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com