Wertbasierte Animationen

Auf dieser Seite wird beschrieben, wie Sie wertbasierte Animationen in Jetpack Compose erstellen. Dabei liegt der Fokus auf APIs, mit denen Werte basierend auf ihrem aktuellen und ihrem Zielstatus animiert werden.

Einzelnen Wert mit animate*AsState animieren

Die animate*AsState-Funktionen sind einfache Animations-APIs in Compose, mit denen ein einzelner Wert animiert werden kann. Sie geben nur den Zielwert (oder Endwert) an und die API startet die Animation vom aktuellen Wert zum angegebenen Wert.

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

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 an der Aufrufstelle gespeichert. Der erste Zielwert wird als Anfangswert verwendet. Wenn Sie diesem zusammensetzbaren Element einen anderen Zielwert zuweisen, wird automatisch eine Animation zu diesem Wert gestartet. Wenn bereits eine Animation ausgeführt wird, beginnt die Animation mit dem aktuellen Wert (und der aktuellen Geschwindigkeit) und wird zum Zielwert animiert. Während der Animation wird dieses zusammensetzbare Element 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 animateValueAsState einen TwoWayConverter zuweisen, der einen generischen Typ verwendet.

Sie können die Animationsspezifikationen anpassen, indem Sie eine 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
}

updateTransition erstellt und speichert eine Instanz von Transition und aktualisiert ihren Status.

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

Anschließend können Sie eine der Erweiterungsfunktionen animate* verwenden, um eine untergeordnete Animation in diesem Übergang zu definieren. Geben Sie die Zielwerte für jeden Status an. Diese animate*-Funktionen geben einen Animationswert zurück, der während der Animation in jedem Frame aktualisiert wird, wenn der Übergangsstatus mit updateTransition 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 Übergangsstatusänderungen eine andere 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
    }
}

Sobald ein Übergang den Zielstatus erreicht hat, ist Transition.currentState mit Transition.targetState identisch. Sie können dies als Signal dafür verwenden, ob der Übergang abgeschlossen ist.

Manchmal möchten Sie möglicherweise einen Anfangsstatus haben, der sich vom ersten Zielstatus unterscheidet. Dazu können Sie updateTransition mit MutableTransitionState verwenden. So können Sie beispielsweise die Animation starten, sobald der Code in die Komposition eintritt.

// 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 verwenden createChildTransition, um einen untergeordneten Übergang zu erstellen. Diese Technik ist nützlich, um die Zuständigkeiten zwischen mehreren Unterkomponenten in einem komplexen zusammensetzbaren Element zu trennen. Der übergeordnete Übergang kennt alle Animationswerte in den untergeordneten Übergängen.

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 Erweiterung funktionen von Transition verfügbar. Der targetState für Transition.AnimatedVisibility und Transition.AnimatedContent wird von Transition abgeleitet und löst bei Bedarf Animationen für Einblenden, Ausblenden und sizeTransform aus, wenn sich der Transition's targetState ändert. Mit diesen Erweiterungsfunktionen können Sie alle Animationen für Einblenden, Ausblenden und sizeTransform, die andernfalls intern für AnimatedVisibility/AnimatedContent wären, in Transition verschieben. Mit diesen Erweiterungsfunktionen können Sie die Statusänderung von AnimatedVisibility/AnimatedContent von außen beobachten. Anstelle eines booleschen visible-Parameters verwendet diese Version von AnimatedVisibility eine Lambda-Funktion, die den Zielstatus des übergeordneten Übergangs in einen booleschen Wert umwandelt.

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

Für einfache Anwendungsfälle ist es eine gute Option, Übergangsanimationen im selben zusammensetzbaren Element wie die UI zu definieren. Wenn Sie jedoch an einer komplexen Komponente mit einer Reihe animierter Werte arbeiten, sollten Sie die Animationsimplementierung von der zusammensetzbaren UI trennen.

Dazu können Sie eine Klasse erstellen, 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) }
}

Unendlich wiederholte Animation mit rememberInfiniteTransition erstellen

InfiniteTransition enthält wie Transition eine oder mehrere untergeordnete Animationen. Die Animationen werden jedoch gestartet, sobald sie in die Komposition eintreten, und werden erst beendet, wenn sie entfernt werden. Sie können mit rememberInfiniteTransition eine Instanz von InfiniteTransition erstellen und mit animateColor, animatedFloat oder animatedValue untergeordnete Animationen 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)
)

Animations-APIs auf niedriger Ebene

Alle im vorherigen Abschnitt erwähnten Animations-APIs auf hoher Ebene basieren auf den Animations-APIs auf niedriger Ebene.

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

updateTransition erstellt ein Übergangsobjekt, mit dem mehrere animierte Werte verwaltet und ausgeführt werden können, wenn sich ein Status ändert. rememberInfiniteTransition ist ähnlich, erstellt aber einen unendlichen Übergang, mit dem mehrere Animationen verwaltet werden können, die unbegrenzt fortgesetzt werden. Alle diese APIs sind zusammensetzbar, 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. Obwohl die meisten Apps nicht direkt mit Animation interagieren, können Sie ü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 Animations-APIs auf niedriger Ebene.

Animatable: Auf Coroutinen basierende Animation für einen einzelnen Wert

Animatable ist ein Wert-Holder, mit dem der Wert bei der Änderung mit animateTo animiert werden kann. Dies ist die API, die die Implementierung von animate*AsState unterstützt. 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 Coroutinen-Bereich einschließen müssen. Sie können beispielsweise das zusammensetzbare Element LaunchedEffect verwenden, um einen Bereich nur für die Dauer des angegebenen Schlüsselwerts zu 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 Color.Red animiert. Jede nachfolgende Änderung des booleschen Werts startet eine Animation zur anderen Farbe. Wenn eine Animation ausgeführt wird, während sich der Wert ändert, bricht Compose die Animation ab 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 genauere 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 zu Grün oder Rot animiert wird.
  • Zweitens bietet Animatable mehr Vorgänge für den Inhaltswert, insbesondere snapTo und animateDecay.
    • snapTo legt den aktuellen Wert sofort auf den Zielwert fest. Dies ist nützlich, wenn die Animation nicht die einzige Quelle der Wahrheit ist und mit anderen Status synchronisiert werden muss, z. B. mit Touch-Ereignissen.
    • animateDecay startet eine Animation, die sich von der angegebenen Geschwindigkeit verlangsamt. Dies ist nützlich, um das Verhalten beim zügigen Wischen zu implementieren.

Weitere Informationen finden Sie unter Gesten und Animationen.

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

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

Animation: Manuell gesteuerte Animation

Animation ist die Animations-API auf der niedrigsten Ebene. Viele der bisher gesehenen Animationen 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 Lebenszykluskonzept. Sie dient als Animationsberechnungs-Engine für APIs auf höherer Ebene.

TargetBasedAnimation

Andere APIs decken die meisten Anwendungsfälle ab. Wenn Sie TargetBasedAnimation jedoch direkt verwenden, können Sie die Wiedergabezeit der Animation steuern. Im folgenden Beispiel steuern Sie die Wiedergabezeit von TargetAnimation manuell basierend auf der Frame-Zeit, die von withFrameNanos bereitgestellt wird.

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 muss für DecayAnimation kein targetValue angegeben werden. Stattdessen wird der targetValue basierend auf den Anfangsbedingungen berechnet, die durch initialVelocity und initialValue sowie die angegebene DecayAnimationSpec festgelegt werden.

Decay-Animationen werden häufig nach einer Wischbewegung verwendet, um Elemente zu verlangsamen und anzuhalten. Die Animationsgeschwindigkeit beginnt mit dem Wert, der von initialVelocityVector festgelegt wird, und verlangsamt sich im Laufe der Zeit.