Wertbasierte Animationen

Einzelnen Wert mit animate*AsState animieren

animate*AsState-Funktionen sind die einfachsten 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 bis zum angegebenen Wert.

Im Folgenden finden Sie ein Beispiel für die Animation einer Alphaversion mit dieser API. Durch das Einfügen des Zielwerts in animateFloatAsState ist der Alphawert jetzt ein Animationswert zwischen den angegebenen Werten (in diesem Fall 1f oder 0.5f).

var enabled by remember { mutableStateOf(true) }

val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
Box(
    Modifier.fillMaxSize()
        .graphicsLayer(alpha = alpha)
        .background(Color.Red)
)

Sie müssen keine Instanz einer Animationsklasse erstellen oder Unterbrechungen verarbeiten. Intern wird ein Animationsobjekt (eine Animatable-Instanz) erstellt und auf der Aufrufwebsite gespeichert, wobei der erste Zielwert der Anfangswert ist. Ab dann wird jedes Mal, wenn Sie für diese zusammensetzbare Funktion einen anderen Zielwert angeben, automatisch eine Animation in Richtung dieses Werts gestartet. Wenn bereits eine Animation läuft, startet die Animation beim aktuellen Wert (und der Geschwindigkeit) und wird in Richtung Zielwert animiert. Während der Animation wird diese zusammensetzbare Funktion neu zusammengesetzt und gibt für jeden Frame einen aktualisierten Animationswert zurück.

Das Tool bietet animate*AsState-Funktionen für Float, Color, Dp, Size, Offset, Rect, Int, IntOffset und IntSize. Sie können ganz einfach andere Datentypen unterstützen. Dazu geben Sie einen TwoWayConverter für animateValueAsState an, der einen generischen Typ verwendet.

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

Mehrere Properties gleichzeitig mit einem Übergang animieren

Transition verwaltet eine oder mehrere Animationen als untergeordnete Elemente und führt sie gleichzeitig zwischen mehreren Zuständen 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 gewährleisten, wie in diesem Beispiel:

enum class BoxState {
    Collapsed,
    Expanded
}

updateTransition erstellt und merkt sich eine Instanz von Transition und aktualisiert ihren Status.

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

Sie können dann eine der animate*-Erweiterungsfunktionen verwenden, um eine untergeordnete Animation für diesen Übergang zu definieren. Geben Sie die Zielwerte für jeden Bundesstaat an. Diese animate*-Funktionen geben einen Animationswert zurück, der während der Animation bei 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 der Änderungen des Übergangsstatus 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
    }
}

Sobald ein Übergang den Zielstatus erreicht hat, ist Transition.currentState mit Transition.targetState identisch. Dies kann als Signal dafür verwendet werden, ob der Übergang abgeschlossen ist.

Es kann vorkommen, dass der Anfangszustand vom ersten Zielstatus abweicht. Dazu können Sie updateTransition mit MutableTransitionState verwenden. So können wir beispielsweise die Animation starten, sobald der Code in die Zusammensetzung eintritt.

// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = updateTransition(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 Bedenken zwischen mehreren Unterkomponenten in einer komplexen zusammensetzbaren Funktion zu trennen. Der übergeordnete Übergang erkennt 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
            }
        )
    }
}

Umstellung 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 nach Bedarf die Eingabe-/Exit-Übergänge aus, wenn sich die targetState der Transition geändert hat. Mit diesen Erweiterungsfunktionen können alle Eingabe-/Exit-/sizeTransform-Animationen, die sonst in AnimatedVisibility/AnimatedContent-intern liegen würden, in die Transition gezogen werden. Mit diesen Erweiterungsfunktionen kann die Statusänderung von AnimatedVisibility/AnimatedContent von außen beobachtet werden. Anstelle eines booleschen visible-Parameters verwendet diese Version von AnimatedVisibility eine Lambda-Funktion, die den Zielstatus der übergeordneten Umstellung in einen booleschen Wert umwandelt.

Weitere Informationen finden Sie unter AnimierteSichtbarkeit und AnimierteContent.

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),
    elevation = 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 durchaus sinnvolle Option, Übergangsanimationen in derselben zusammensetzbaren Funktion wie auf der Benutzeroberfläche zu definieren. Wenn Sie jedoch an einer komplexen Komponente mit mehreren animierten Werten arbeiten, kann es sinnvoll sein, die Animationsimplementierung von der zusammensetzbaren UI zu trennen.

Dazu können Sie eine Klasse erstellen, die alle Animationswerte enthält, sowie eine Aktualisierungsfunktion, die eine Instanz dieser Klasse zurückgibt. Die Transition-Implementierung kann in die neue separate Funktion extrahiert werden. Dieses Muster ist nützlich, wenn die Animationslogik zentralisiert werden muss oder komplexe Animationen wiederverwendbar gemacht werden 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 endlos wiederkehrende Animation erstellen

InfiniteTransition enthält eine oder mehrere untergeordnete Animationen wie Transition. Die Animationen werden jedoch ausgeführt, sobald sie in die Komposition aufgenommen wurden, und werden erst beendet, wenn sie entfernt werden. Sie können eine Instanz von InfiniteTransition mit rememberInfiniteTransition erstellen. Untergeordnete Animationen können mit animateColor, animatedFloat oder animatedValue hinzugefügt werden. Außerdem müssen Sie infiniteRepeatable angeben, um die Animationsspezifikationen festzulegen.

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

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

Low-Level-Animations-APIs

Alle High-Level-Animations-APIs, die im vorherigen Abschnitt erwähnt wurden, basieren auf den Low-Level-Animations-APIs.

animate*AsState-Funktionen sind die einfachsten APIs, die eine sofortige Wertänderung als Animationswert rendern. Es wird von Animatable unterstützt, einer Koroutinen-basierten API zum Animieren eines einzelnen Werts. updateTransition erstellt ein Übergangsobjekt, das mehrere animierte Werte verwalten und bei einer Statusänderung ausführen kann. rememberInfiniteTransition ist ähnlich, erzeugt jedoch einen unendlichen Übergang, mit dem mehrere Animationen verwaltet werden können, die unbegrenzt laufen. Alle diese APIs sind zusammensetzbare Funktionen, mit Ausnahme von Animatable. Das bedeutet, dass diese Animationen auch außerhalb der Zusammensetzung erstellt werden können.

Alle diese APIs basieren auf der grundlegenderen Animation API. Obwohl die meisten Apps nicht direkt mit Animation interagieren, sind einige der Anpassungsfunktionen für Animation über übergeordnete APIs verfügbar. Weitere Informationen zu AnimationVector und AnimationSpec findest du unter Animationen anpassen.

Diagramm, das die Beziehung zwischen den verschiedenen Low-Level-Animations-APIs zeigt

Animatable: Koroutinenbasierte Einzelwertanimation

Animatable ist ein Werthalter, der den Wert animieren kann, wenn er über animateTo geändert wird. Dies ist die API, die die Implementierung von animate*AsState sichert. Sie sorgt für eine konsistente Fortsetzung und gegenseitige Exklusivität, d. h., die Wertänderung erfolgt immer kontinuierlich und alle laufenden Animationen werden abgebrochen.

Viele Features von Animatable, einschließlich animateTo, werden als Sperrfunktionen bereitgestellt. Das bedeutet, dass sie in einen geeigneten Koroutinebereich eingeschlossen werden müssen. Sie können beispielsweise die zusammensetzbare Funktion 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 obigen Beispiel wird eine Instanz von Animatable mit dem Anfangswert Color.Gray erstellt und gespeichert. Abhängig vom Wert des booleschen Flags ok wird die Farbe entweder zu Color.Green oder Color.Red animiert. Bei jeder nachfolgenden Änderung des booleschen Werts wird die Animation zur anderen Farbe gestartet. Wenn beim Ändern des Werts eine Animation aktiv ist, wird die Animation abgebrochen und die neue Animation beginnt beim aktuellen Snapshot-Wert mit der aktuellen Geschwindigkeit.

Dies ist die Animationsimplementierung, mit der die im vorherigen Abschnitt erwähnte animate*AsState API gesichert wird. Im Vergleich zu animate*AsState erhalten wir durch die direkte Verwendung von Animatable eine feinere Kontrolle in mehreren Bereichen. Erstens kann für Animatable ein Anfangswert festgelegt werden, der sich vom ersten Zielwert unterscheidet. Das Codebeispiel oben zeigt beispielsweise zuerst ein graues Feld, das sofort zu Grün oder Rot animiert wird. Zweitens bietet Animatable weitere Vorgänge für den Inhaltswert, nämlich snapTo und animateDecay. snapTo setzt den aktuellen Wert sofort auf den Zielwert. Das ist nützlich, wenn die Animation selbst nicht die einzige Quelle der Wahrheit ist und mit anderen Status, wie z. B. Touch-Ereignissen, synchronisiert werden muss. animateDecay startet eine Animation, die ausgehend von der angegebenen Geschwindigkeit langsamer wird. Dies ist nützlich, um das Schleuder-Verhalten zu implementieren. Weitere Informationen finden Sie unter Gesten und Animationen.

Animatable unterstützt standardmäßig Float und Color. Durch Angabe von TwoWayConverter kann jedoch jeder Datentyp verwendet werden. 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 unterste verfügbare Animation API. Viele der Animationen, die wir bisher gesehen haben, bauen auf Animationen auf. Es gibt zwei Animation-Untertypen: TargetBasedAnimation und DecayAnimation.

Animation sollte nur verwendet werden, um die Zeit der Animation manuell zu steuern. Animation ist zustandslos und hat kein Konzept für einen Lebenszyklus. Sie dient als Berechnungs-Engine für Animationen, die von den APIs der höheren Ebene verwendet wird.

TargetBasedAnimation

Andere APIs decken die meisten Anwendungsfälle ab, aber wenn Sie TargetBasedAnimation verwenden, können Sie die Wiedergabedauer der Animation selbst steuern. Im folgenden Beispiel wird die Abspielzeit von TargetAnimation manuell anhand der von withFrameNanos bereitgestellten Framezeit gesteuert.

val anim = remember {
    TargetBasedAnimation(
        animationSpec = tween(200),
        typeConverter = Float.VectorConverter,
        initialValue = 200f,
        targetValue = 1000f
    )
}
var playTime by remember { mutableStateOf(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 keine targetValue angegeben werden. Stattdessen wird die targetValue anhand der Startbedingungen berechnet, die von initialVelocity und initialValue sowie dem bereitgestellten DecayAnimationSpec festgelegt wurden.

Abklinganimationen werden oft nach einer geschleiften Bewegung verwendet, um die Elemente bis zum Ende zu verlangsamen. Die Animationsgeschwindigkeit beginnt bei dem durch initialVelocityVector festgelegten Wert und wird im Laufe der Zeit langsamer.