Wertbasierte Animationen

Auf dieser Seite wird beschrieben, wie Sie wertbasierte Animationen in Jetpack Compose erstellen. Der Schwerpunkt liegt dabei auf APIs, die Werte basierend auf ihrem aktuellen und ihrem Zielstatus animieren.

Einen einzelnen Wert mit animate*AsState animieren

Die Funktionen animate*AsState sind einfache Animations-APIs in Compose zum Animieren eines einzelnen Werts. Sie geben nur den Zielwert (oder Endwert) an und die API startet die Animation vom aktuellen Wert zum angegebenen Wert.

Im folgenden Beispiel wird der Alphawert mithilfe dieser API animiert. Wenn Sie den Zielwert in animateFloatAsState einschließen, ist der Alphawert jetzt ein Animationswert zwischen den angegebenen Werten (in diesem Fall 1f oder 0.5f).

var enabled by remember { mutableStateOf(true) }

val animatedAlpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha")
Box(
    Modifier
        .fillMaxSize()
        .graphicsLayer { alpha = animatedAlpha }
        .background(Color.Red)
)

Sie müssen keine Instanz einer Animationsklasse erstellen oder Unterbrechungen verarbeiten. Im Hintergrund wird ein Animationsobjekt (eine Animatable-Instanz) erstellt und am Aufrufort gespeichert. Der erste Zielwert wird als Anfangswert verwendet. Wenn Sie diesem Composable einen anderen Zielwert übergeben, wird automatisch eine Animation für diesen Wert gestartet. Wenn bereits eine Animation läuft, beginnt die Animation mit dem aktuellen Wert (und der aktuellen Geschwindigkeit) und wird in Richtung des Zielwerts animiert. Während der Animation wird diese Composable neu zusammengesetzt und gibt in jedem Frame einen aktualisierten Animationswert zurück.

Standardmäßig bietet Compose animate*AsState-Funktionen für Float, Color, Dp, Size, Offset, Rect, Int, IntOffset und IntSize. Sie können Unterstützung für andere Datentypen hinzufügen, indem Sie eine TwoWayConverter-Funktion für animateValueAsState bereitstellen, die einen generischen Typ akzeptiert.

Sie können die Animationsspezifikationen anpassen, indem Sie ein AnimationSpec angeben. Weitere Informationen finden Sie unter AnimationSpec.

Mehrere Eigenschaften gleichzeitig mit einem Übergang animieren

Transition verwaltet eine oder mehrere Animationen als untergeordnete Elemente und führt sie gleichzeitig zwischen mehreren Status aus.

Die Status können einen beliebigen Datentyp haben. In vielen Fällen können Sie einen benutzerdefinierten enum-Typ verwenden, um die Typsicherheit zu überprüfen, wie in diesem Beispiel:

enum class BoxState {
    Collapsed,
    Expanded
}

Mit updateTransition wird eine Instanz von Transition erstellt und gespeichert und ihr Status wird aktualisiert.

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "box state")

Anschließend können Sie eine der animate*-Erweiterungsfunktionen verwenden, um eine untergeordnete Animation in diesem Übergang zu definieren. Geben Sie die Zielwerte für die einzelnen Bundesstaaten an. Diese animate*-Funktionen geben einen Animationswert zurück, der bei jeder Aktualisierung des Übergangszustands mit updateTransition in jedem Frame der Animation aktualisiert wird.

val rect by transition.animateRect(label = "rectangle") { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = "border width") { state ->
    when (state) {
        BoxState.Collapsed -> 1.dp
        BoxState.Expanded -> 0.dp
    }
}

Optional können Sie einen transitionSpec-Parameter übergeben, um für jede Kombination von Übergangszustandsänderungen einen anderen AnimationSpec anzugeben. Weitere Informationen finden Sie unter AnimationSpec.

val color by transition.animateColor(
    transitionSpec = {
        when {
            BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
                spring(stiffness = 50f)

            else ->
                tween(durationMillis = 500)
        }
    }, label = "color"
) { state ->
    when (state) {
        BoxState.Collapsed -> MaterialTheme.colorScheme.primary
        BoxState.Expanded -> MaterialTheme.colorScheme.background
    }
}

Wenn ein Übergang den Zielstatus erreicht hat, ist Transition.currentState gleich Transition.targetState. Daran können Sie erkennen, ob die Umstellung abgeschlossen ist.

Manchmal möchten Sie möglicherweise einen anderen Anfangszustand als den ersten Zielzustand haben. Dazu können Sie updateTransition mit MutableTransitionState verwenden. So können Sie beispielsweise eine Animation starten, sobald der Code in die Komposition aufgenommen wird.

// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = rememberTransition(currentState, label = "box state")
// ……

Für einen komplexeren Übergang mit mehreren zusammensetzbaren Funktionen können Sie mit createChildTransition einen untergeordneten Übergang erstellen. Diese Technik ist nützlich, um die Verantwortlichkeiten zwischen mehreren Unterkomponenten in einer komplexen zusammensetzbaren Funktion zu trennen. Die übergeordnete Transition kennt alle Animationswerte in den untergeordneten Transitions.

enum class DialerState { DialerMinimized, NumberPad }

@Composable
fun DialerButton(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun NumberPad(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun Dialer(dialerState: DialerState) {
    val transition = updateTransition(dialerState, label = "dialer state")
    Box {
        // Creates separate child transitions of Boolean type for NumberPad
        // and DialerButton for any content animation between visible and
        // not visible
        NumberPad(
            transition.createChildTransition {
                it == DialerState.NumberPad
            }
        )
        DialerButton(
            transition.createChildTransition {
                it == DialerState.DialerMinimized
            }
        )
    }
}

Übergang mit AnimatedVisibility und AnimatedContent verwenden

AnimatedVisibility und AnimatedContent sind als Erweiterungsfunktionen von Transition verfügbar. Die targetState für Transition.AnimatedVisibility und Transition.AnimatedContent wird aus der Transition abgeleitet und löst bei Bedarf Ein-, Aus- und sizeTransform-Animationen aus, wenn sich die targetState der Transition ändert. Mit diesen Erweiterungsfunktionen können Sie alle Ein-, Aus- und sizeTransform-Animationen, die sonst intern für AnimatedVisibility/AnimatedContent wären, in die Transition verschieben. Mit diesen Erweiterungsfunktionen können Sie den Statuswechsel von AnimatedVisibility/AnimatedContent von außen beobachten. Anstelle eines booleschen visible-Parameters wird in dieser Version von AnimatedVisibility ein Lambda verwendet, das den Zielstatus des übergeordneten Übergangs in einen booleschen Wert konvertiert.

Weitere Informationen finden Sie unter AnimatedVisibility und AnimatedContent.

var selected by remember { mutableStateOf(false) }
// Animates changes when `selected` is changed.
val transition = updateTransition(selected, label = "selected state")
val borderColor by transition.animateColor(label = "border color") { isSelected ->
    if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp(label = "elevation") { isSelected ->
    if (isSelected) 10.dp else 2.dp
}
Surface(
    onClick = { selected = !selected },
    shape = RoundedCornerShape(8.dp),
    border = BorderStroke(2.dp, borderColor),
    shadowElevation = elevation
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Text(text = "Hello, world!")
        // AnimatedVisibility as a part of the transition.
        transition.AnimatedVisibility(
            visible = { targetSelected -> targetSelected },
            enter = expandVertically(),
            exit = shrinkVertically()
        ) {
            Text(text = "It is fine today.")
        }
        // AnimatedContent as a part of the transition.
        transition.AnimatedContent { targetState ->
            if (targetState) {
                Text(text = "Selected")
            } else {
                Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone")
            }
        }
    }
}

Übergang kapseln und wiederverwendbar machen

Bei einfachen Anwendungsfällen ist es eine gute Option, Übergangsanimationen im selben Composable wie Ihre Benutzeroberfläche zu definieren. Wenn Sie jedoch an einer komplexen Komponente mit einer Reihe animierter Werte arbeiten, sollten Sie die Animationsimplementierung möglicherweise von der zusammensetzbaren Benutzeroberfläche trennen.

Dazu erstellen Sie eine Klasse, die alle Animationswerte enthält, und eine update-Funktion, die eine Instanz dieser Klasse zurückgibt. Sie können die Übergangsimplementierung in die neue separate Funktion extrahieren. Dieses Muster ist nützlich, wenn Sie die Animationslogik zentralisieren oder komplexe Animationen wiederverwendbar machen müssen.

enum class BoxState { Collapsed, Expanded }

@Composable
fun AnimatingBox(boxState: BoxState) {
    val transitionData = updateTransitionData(boxState)
    // UI tree
    Box(
        modifier = Modifier
            .background(transitionData.color)
            .size(transitionData.size)
    )
}

// Holds the animation values.
private class TransitionData(
    color: State<Color>,
    size: State<Dp>
) {
    val color by color
    val size by size
}

// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
    val transition = updateTransition(boxState, label = "box state")
    val color = transition.animateColor(label = "color") { state ->
        when (state) {
            BoxState.Collapsed -> Color.Gray
            BoxState.Expanded -> Color.Red
        }
    }
    val size = transition.animateDp(label = "size") { state ->
        when (state) {
            BoxState.Collapsed -> 64.dp
            BoxState.Expanded -> 128.dp
        }
    }
    return remember(transition) { TransitionData(color, size) }
}

Mit rememberInfiniteTransition eine Animation erstellen, die sich unendlich oft wiederholt

InfiniteTransition enthält eine oder mehrere untergeordnete Animationen wie Transition. Die Animationen werden jedoch sofort nach dem Einfügen in die Komposition gestartet und erst beendet, wenn sie entfernt werden. Sie können eine Instanz von InfiniteTransition mit rememberInfiniteTransition erstellen und untergeordnete Animationen mit animateColor, animatedFloat oder animatedValue hinzufügen. Außerdem müssen Sie eine infiniteRepeatable angeben, um die Animationsspezifikationen festzulegen.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Red,
    targetValue = Color.Green,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

Low-Level-APIs für Animationen

Alle im vorherigen Abschnitt erwähnten High-Level-Animations-APIs basieren auf den Low-Level-Animations-APIs.

Die animate*AsState-Funktionen sind einfache APIs, die eine sofortige Wertänderung als Animationswert rendern. Diese Funktion basiert auf Animatable, einer auf Coroutinen basierenden API zum Animieren eines einzelnen Werts.

updateTransition erstellt ein Übergangsobjekt, das mehrere animierte Werte verwalten und bei einer Zustandsänderung ausführen kann. rememberInfiniteTransition ist ähnlich, erstellt aber einen unendlichen Übergang, der mehrere Animationen verwalten kann, die auf unbestimmte Zeit fortgesetzt werden. Alle diese APIs sind Composables, mit Ausnahme von Animatable. Das bedeutet, dass Sie diese Animationen außerhalb der Komposition erstellen können.

Alle diese APIs basieren auf der grundlegenderen Animation API. Die meisten Apps interagieren nicht direkt mit Animation, aber Sie können über APIs auf höherer Ebene auf einige der Anpassungsfunktionen zugreifen. Weitere Informationen zu AnimationVector und AnimationSpec finden Sie unter Animationen anpassen.

Beziehung zwischen untergeordneten Animations-APIs
Abbildung 1: Beziehung zwischen untergeordneten Animations-APIs.

Animatable: Coroutine-basierte Animation mit einem einzelnen Wert

Animatable ist ein Wert-Holder, der den Wert animieren kann, wenn er mit animateTo geändert wird. Dies ist die API, die der Implementierung von animate*AsState zugrunde liegt. Sie sorgt für eine konsistente Fortsetzung und gegenseitige Ausschließlichkeit. Das bedeutet, dass die Wertänderung immer kontinuierlich ist und Compose alle laufenden Animationen abbricht.

Viele Funktionen von Animatable, einschließlich animateTo, sind suspend-Funktionen. Das bedeutet, dass Sie sie in einen geeigneten Coroutine-Bereich einfügen müssen. Mit der zusammensetzbaren Funktion LaunchedEffect können Sie beispielsweise einen Bereich nur für die Dauer des angegebenen Schlüsselwerts erstellen.

// Start out gray and animate to green/red based on `ok`
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
    color.animateTo(if (ok) Color.Green else Color.Red)
}
Box(
    Modifier
        .fillMaxSize()
        .background(color.value)
)

Im vorherigen Beispiel erstellen und speichern Sie eine Instanz von Animatable mit dem Anfangswert Color.Gray. Je nach Wert des booleschen Flags ok wird die Farbe entweder zu Color.Green oder zu Color.Red animiert. Jede nachfolgende Änderung des booleschen Werts startet eine Animation zur anderen Farbe. Wenn eine Animation ausgeführt wird, wenn sich der Wert ändert, wird die Animation in Compose abgebrochen und die neue Animation beginnt mit dem aktuellen Snapshot-Wert und der aktuellen Geschwindigkeit.

Diese Animatable-API ist die zugrunde liegende Implementierung für animate*AsState, die im vorherigen Abschnitt erwähnt wurde. Die direkte Verwendung von Animatable bietet in mehrfacher Hinsicht eine detailliertere Steuerung:

  • Erstens kann Animatable einen Anfangswert haben, der sich vom ersten Zielwert unterscheidet. Im vorherigen Codebeispiel wird beispielsweise zuerst ein graues Feld angezeigt, das sofort entweder grün oder rot animiert wird.
  • Zweitens bietet Animatable mehr Vorgänge für den Inhaltswert, insbesondere snapTo und animateDecay.
    • Mit snapTo wird der aktuelle Wert sofort auf den Zielwert festgelegt. Das ist nützlich, wenn die Animation nicht die einzige Quelle der Wahrheit ist und mit anderen Status wie Touch-Ereignissen synchronisiert werden muss.
    • Mit animateDecay wird eine Animation gestartet, die sich von der angegebenen Geschwindigkeit verlangsamt. Dies ist nützlich, um das Verhalten beim schnellen Wischen zu implementieren.

Weitere Informationen finden Sie unter Gesten und Animationen.

Standardmäßig werden in Animatable die Datentypen Float und Color unterstützt. Sie können jedoch einen beliebigen Datentyp verwenden, indem Sie ein TwoWayConverter angeben. Weitere Informationen finden Sie unter AnimationVector.

Sie können die Animationsspezifikationen anpassen, indem Sie ein AnimationSpec angeben. Weitere Informationen finden Sie unter AnimationSpec.

Animation: Manuell gesteuerte Animation

Animation ist die Animation API auf der niedrigsten Ebene. Viele der Animationen, die wir bisher gesehen haben, basieren auf Animation. Es gibt zwei Animation-Untertypen: TargetBasedAnimation und DecayAnimation.

Verwenden Sie Animation nur, um die Zeit der Animation manuell zu steuern. Animation ist zustandslos und hat kein Konzept für den Lebenszyklus. Sie dient als Berechnungs-Engine für Animationen für APIs auf höherer Ebene.

TargetBasedAnimation

Die meisten Anwendungsfälle werden von anderen APIs abgedeckt, aber mit TargetBasedAnimation können Sie die Wiedergabezeit der Animation direkt steuern. Im folgenden Beispiel steuern Sie die Wiedergabezeit von TargetAnimation manuell anhand der von withFrameNanos bereitgestellten Frame-Zeit.

val anim = remember {
    TargetBasedAnimation(
        animationSpec = tween(200),
        typeConverter = Float.VectorConverter,
        initialValue = 200f,
        targetValue = 1000f
    )
}
var playTime by remember { mutableLongStateOf(0L) }

LaunchedEffect(anim) {
    val startTime = withFrameNanos { it }

    do {
        playTime = withFrameNanos { it } - startTime
        val animationValue = anim.getValueFromNanos(playTime)
    } while (someCustomCondition())
}

DecayAnimation

Im Gegensatz zu TargetBasedAnimation ist für DecayAnimation keine targetValue erforderlich. Stattdessen wird der targetValue anhand der Startbedingungen berechnet, die von initialVelocity und initialValue festgelegt werden, sowie anhand des angegebenen DecayAnimationSpec.

Decay-Animationen werden oft nach einer Fling-Geste verwendet, um Elemente zu verlangsamen, bis sie zum Stillstand kommen. Die Animationsgeschwindigkeit beginnt mit dem von initialVelocityVector festgelegten Wert und verlangsamt sich im Laufe der Zeit.