アニメーションの状態を超えて


Jetpackは数週間前にヒット1.0を作成し、それは素晴らしいと堅牢なアニメーションAPIが付属しています.この新しいAPIを使用すると、アニメーションの状態を変更するときに完全に制御する必要があります.しかし、より複雑なシナリオになると、いくつかの明白なパスに直面することがあります.この記事は、これらのシナリオのいくつかを探求し、どのようにあなたの目標を達成できるかを理解するのに役立ちます.
私は、私がそれを実行しようとするとき、私が見つけた問題について議論しようとしていますAVLoadingIndicatorView 図書館.そして、この旅を案内するために、例として以下の読み込み指標を使用します.

また、この記事で議論するすべてのコードは、このリポジトリにあります.

ファゴグスタボ / 負荷インジケータ



BloscaleIndicator


簡単なアニメーションから始めましょう.アニメーションは、円からスケールを上げながらアルファを減らすことにある.0から1に移動する1つの値でそれを行うことができます.したがって、スケールは現在の値になります、そして、アルファは相補値です1 - currentValue ). これはローディングアニメーションですので、我々は永遠に繰り返すように設定する必要があります.
TLドクターこうする必要があります.
@Composable
fun BallScaleIndicator() {
    val animationProgress by animateFloatAsState(
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 800)
        )
    )

    Ball(
        modifier = Modifier
            .scale(animationProgress)
            .alpha(1 - animationProgress),
    )
}
しかし、あなたのアプリケーションを実行すると、これは結果です.

何も起こらない.なぜ?この記事からの最初の行では、構成が状態変化をアニメーション化するためにものすごいAPIを持っていると言いました、しかし、これは若干の州の変化でありません.ターゲットの状態を制御してアニメーションを開始するために変数を1つ追加する必要があります.また、ビューがレンダリングされると、アニメーションを開始する値を変更する必要があります.
@Composable
fun BallScaleIndicator() {
    // Create one target value state that will 
    // change to start the animation
    var targetValue by remember { mutableStateOf(0f) }

    // Update the attribute on the animation
    val animationProgress by animateFloatAsState(
        targetValue = targetValue,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 800)
        )
    )

    // Use the SideEffect helper to run something
    // when this block runs
    SideEffect { targetValue = 1f }

    Ball(
        modifier = Modifier
            .scale(animationProgress)
            .alpha(1 - animationProgress),
    )
}
そして今、すべての作品🎉

しかし、我々にはもう一つの問題があります.The SideEffect ビューが再構成されるたびに実行されます.アニメーションが各反復の状態値を変更するように、この効果を何度も実行します.これはアニメーション作業を行うが、それはうまくスケールしない可能性がありますいくつかのUIの問題を引き起こす.
また、遷移を使用して値をアニメーション化する方法を提供します.遷移の1つのタイプは無限遷移です.それはあなたのパラメータとして初期値と最終的な値を使用して構文を提供し、自動的に作成されたとき(副作用の必要はありません)が自動的に開始されます.それを使用するには、rememberInfiniteTransition メソッドと呼び出しanimateFloat アニメーション状態を持つ関数.
小さなリファクタの後、これは結果です.
@Composable
fun BallScaleIndicator() {
    // Creates the infinite transition
    val infiniteTransition = rememberInfiniteTransition()

    // Animate from 0f to 1f
    val animationProgress by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 800)
        )
    )

    Ball(
        modifier = Modifier
            .scale(animationProgress)
            .alpha(1 - animationProgress),
    )
}
そして、結果アニメーションは同じですが、この機能に副作用を使用せずに.

バルプルセスナー


さて、我々は最初の1つを得た.もっと複雑にしましょう.これは3つのボールの同期でジャンプで構成されます.これを達成する最も簡単な方法は、各アニメーションの開始を遅らせることです.我々は600 msのアニメーション時間を持つことができて、各々のボールのために70 msの遅れでそれを開始します.
アニメーションアニメーションAPIをすばやく検索するにはtween アニメーションのプロパティdelayMillis この動作を実装するために使用できます.そして、値をアニメーション化するために、無限の遷移を保つことができます.では、作業を始めましょう.
@Composable
fun BallPulseSyncIndicator() {
    val infiniteTransition = rememberInfiniteTransition()
    val animationValues = (1..3).map { index ->
        infiniteTransition.animateFloat(
            initialValue = 0f,
            targetValue = 12f,
            animationSpec = infiniteRepeatable(
                animation = tween(
                    durationMillis = 300,
                    delayMillis = 70 * index,
                ),
                repeatMode = RepeatMode.Reverse,
            )
        )
    }

    Row {
        animationValues.forEach { animatedValue ->
            Ball(
                modifier = Modifier
                    .padding(horizontal = 4.dp)
                    .offset(y = animatedValue.value.dp),
            )
        }
    }
}
いい音、右?しかし、我々がアニメーションを見るとき、我々は何か変なものに気がつきます.

いくつかの実行時間の後、アニメーションが同期を失い、奇妙に振る舞うことを見ることができます.その理由は、アニメーションを遅延させるために使用したプロパティです.これは、最初の1つだけではなく、各繰り返しに遅延を適用します.
解決策は、CoroutinesアニメーションAPIを使用することです.これは、アニメーションの作成によって提供され、メソッドを呼ばれるanimate . この構文は、animateFloat 移行から.それを念頭に置いて、我々はdelay アニメーションを開始する前に、Coroutinesからの機能.これは正しい動作を保証します.
@Composable
fun BallPulseSyncIndicator() {
    val animationValues = (1..3).map { index ->
        var animatedValue by remember { mutableStateOf(0f) }

        LaunchedEffect(key1 = Unit) {
            // Delaying using Coroutines
            delay(70L * index)

            animate(
                initialValue = 0f,
                targetValue = 12f,
                animationSpec = infiniteRepeatable(
                    // Remove delay property
                    animation = tween(durationMillis = 300),
                    repeatMode = RepeatMode.Reverse,
                )
            ) { value, _ -> animatedValue = value }
        }

        animatedValue
    }

    Row {
        animationValues.forEach { animatedValue ->
            Ball(
                modifier = Modifier
                    .padding(horizontal = 4.dp)
                    .offset(y = animatedValue.dp),
            )
        }
    }
}
今、アニメーションはいくつかの時間後も同期を維持します.

三角レンズ


よし、次の方に行こう.この三角形のインジケータは2つのアニメーション(X軸とY軸上の回転)を持っていますが、あなたは次のものを始めるために実行する前のものを待つ必要があります.それで、タイムラインでそれを置くならば、我々はこのように何かを持ちます

このような何かを評価する最も簡単な方法は、アニメーションをグループで一つのものとして扱うことです.各グループは、リスト内の値と次の項目で構成され、評価するために同じ量を取る.

イメージを書き換えるには、次のようにします.
@Composable
fun animateValues(
    values: List<Float>,
    animationSpec: AnimationSpec<Float> = spring(),
): State<Float> {
    // 1. Create the groups zipping with next entry
    val groups by rememberUpdatedState(newValue = values.zipWithNext())

    // 2. Start the state with the first value
    val state = remember { mutableStateOf(values.first()) }

    LaunchedEffect(key1 = groups) {
        val (_, setValue) = state

        // Start the animation from 0 to groups quantity
        animate(
            initialValue = 0f,
            targetValue = groups.size.toFloat(),
            animationSpec = animationSpec,
        ) { frame, _ ->
            // Get which group is being evaluated
            val integerPart = frame.toInt()
            val (initialValue, finalValue) = groups[frame.toInt()]

            // Get the current "position" from the group animation
            val decimalPart = frame - integerPart

            // Calculate the progress between the initial and final value
            setValue(
                initialValue + (finalValue - initialValue) * decimalPart
            )
        }
    }

    return state
}
これを実装すると、アニメーションプロセスはかなり簡単になります.ちょうどXとYの回転を保持する2つの変数を作成し、ビューを使用して更新.graphicsLayer 修飾子.
@Composable
fun TriangleSkewSpinIndicator() {
    val animationSpec = infiniteRepeatable<Float>(
        animation = tween(
            durationMillis = 2500,
            easing = LinearEasing,
        )
    )
    val xRotation by animateValues(
        values = listOf(0f, 180f, 180f, 0f, 0f), 
        animationSpec = animationSpec
    )
    val yRotation by animateValues(
        values = listOf(0f, 0f, 180f, 180f, 0f), 
        animationSpec = animationSpec
    )

    Triangle(
        modifier = Modifier.graphicsLayer(
            rotationX = xRotation,
            rotationY = yRotation,
        )
    )
}
これが結果です.

最後の思考


Jetpackの構成は信じられないほどのアニメーションAPIが付属しています.それはあなたが必要なアニメーションのすべての種類を実装するために多くの方法を提供します.しかし、現在のAPIは命令的なバージョンと少し異なります.
あなたが作成するスムーズな移行を支援するために、TouchLabは、これらすべての非明らかなパスを支援するためのプロジェクトを開始しました.

タッチラボラボ / アニメーションの作成



アニメーションの作成


ライブラリのグループを使用してより良いアニメーションを構築するJetpack Compose

アバウト


ゴール


このプロジェクトの主な目標は、Jetpack Composeとアニメーションを構築するためです.作文
アニメーションAPIは、状態変更を処理するために豊富なアニメーションAPIを提供しますが、実装する必要があります
いくつかのボイラープレートのコードは、他の種類のアニメーションになるとき.

何が含まれていますか。


  • Easings - あなたのアニメーションで使用する30種類の関連付けとライブラリ

  • Value Animator Compat - 非合成アニメーションとの互換性API
    Valueanimatorの使用

  • Value Animator - からの同じ機能を提供するAPI
    非人工の値を作成しますが、
  • タッチラボについて


    TouchLabは、ニューヨークに拠点を置くモバイルフォーカス開発エージェンシーです.以来、我々はAndroidで作業している
    最初の、そして、過去10年間の広範囲のモバイルとハードウェアプロジェクトに取り組みました
    過去数年間、我々は大幅に投資している.
    View on GitHub
    読書ありがとう!あなたが質問をするならば、コメントで知らせてください.また、あなたは私に手を差し伸べることができます
    Kotlin Slack , or AndroidDevBr Slack . そして、あなたがこのすべてを見つけるならば、多分あなたはwork with or work at TouchLab