Wertbasierte Animationen

Einen einzelnen Wert mit animate*AsState animieren

Die 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 Wert bis zum angegebenen Wert.

Unten sehen Sie ein Beispiel für die Alpha-Animation mit dieser API. Durch das Einschließen 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, label = "alpha")
Box(
    Modifier
        .fillMaxSize()
        .graphicsLayer(alpha = alpha)
        .background(Color.Red)
)

Sie müssen keine Instanz einer Animationsklasse erstellen und auch keine Unterbrechungen verwalten. Im Hintergrund wird ein Animationsobjekt (nämlich eine Animatable-Instanz) an der Aufrufstelle erstellt und mit dem ersten Zielwert als Anfangswert gespeichert. Jedes Mal, wenn Sie dieser zusammensetzbaren Funktion einen anderen Zielwert zuweisen, wird automatisch eine Animation in Richtung dieses Werts gestartet. Wenn bereits eine Animation läuft, beginnt die Animation beim aktuellen Wert (und der Geschwindigkeit) und wird in Richtung des Zielwerts animiert. Während der Animation wird dieses Composeable 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 die Unterstützung für andere Datentypen ganz einfach hinzufügen, indem Sie TwoWayConverter bis animateValueAsState mit einem generischen Typ angeben.

Sie können die Animationsangaben 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 in mehreren Status aus.

Die Status können beliebigen Datentypen 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 speichert 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 bei diesem Übergang eine untergeordnete Animation zu definieren. Geben Sie die Zielwerte für jeden der Status an. Diese animate*-Funktionen geben einen Animationswert zurück, der während der Animation jeden 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 der Kombinationen von Übergangsstatusä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
    }
}

Sobald eine Transition den Zielstatus erreicht hat, ist Transition.currentState mit Transition.targetState identisch. Dies kann als Signal dafür verwendet werden, ob die Umstellung abgeschlossen ist.

Manchmal möchten wir einen Anfangsstatus, der sich vom ersten Zielstatus unterscheidet. Dazu können wir updateTransition mit MutableTransitionState verwenden. So können wir z. B. die Animation starten, sobald der Code die Zusammensetzung eingegeben hat.

// 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 createChildTransition verwenden, um einen untergeordneten Übergang zu erstellen. Diese Technik ist nützlich, um Probleme in mehreren Unterkomponenten in einer komplexen Composeable-Anwendung 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
            }
        )
    }
}

Übergänge 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 Transition abgeleitet und löst bei Bedarf Ein-/Ausstiegsübergänge aus, wenn sich der targetState der Transition geändert hat. Mit diesen Erweiterungsfunktionen können alle „enter/exit/sizeTransform“-Animationen, die sonst intern in AnimatedVisibility/AnimatedContent wären, in die Transition verschoben werden. Mit diesen Erweiterungsfunktionen kann der Status von AnimatedVisibility/AnimatedContent von außen beobachtet werden. 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 animateVisibility und animateContent.

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")
            }
        }
    }
}

Übergänge kapseln und wiederverwendbar machen

Bei einfachen Anwendungsfällen ist es durchaus sinnvoll, Übergangsanimationen im selben Composeable wie Ihre Benutzeroberfläche zu definieren. Wenn Sie jedoch an einer komplexen Komponente mit einer Reihe von animierten Werten arbeiten, sollten Sie die Animationsimplementierung 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. Die Übergangsimplementierung kann in die neue separate Funktion extrahiert werden. Dieses Muster ist nützlich, wenn die Animationslogik zentralisiert werden muss oder komplexe Animationen wiederverwendbar 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) }
}

Endlos wiederholbare Animation mit rememberInfiniteTransition erstellen

InfiniteTransition enthält eine oder mehrere untergeordnete Animationen wie Transition. Die Animationen werden jedoch ausgeführt, sobald sie in die Komposition eintreten, und enden erst, 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 anzugeben.

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-Animation-APIs

Alle im vorherigen Abschnitt genannten APIs für die Animation auf höherer Ebene bauen auf den Low-Level-Animation-APIs auf.

Die animate*AsState-Funktionen sind die einfachsten APIs, die eine sofortige Wertänderung als Animationswert rendern. Sie wird von Animatable unterstützt, einer coroutinebasierten API zum Animieren eines einzelnen Werts. Mit updateTransition wird ein Übergangsobjekt erstellt, mit dem mehrere animierte Werte verwaltet und basierend auf einem Statuswechsel ausgeführt werden können. rememberInfiniteTransition ist ähnlich, erstellt aber einen unendlichen Übergang, mit dem mehrere Animationen verwaltet werden können, die unbegrenzt laufen. Alle diese APIs sind zusammensetzbar, mit Ausnahme von Animatable. Das bedeutet, dass diese Animationen außerhalb der Komposition erstellt werden können.

Alle diese APIs basieren auf der grundlegenden Animation API. Die meisten Apps interagieren nicht direkt mit Animation. Einige der Anpassungsfunktionen für Animation sind jedoch über APIs höherer Ebene verfügbar. Weitere Informationen zu AnimationVector und AnimationSpec finden Sie unter Animationen anpassen.

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

Animatable: Koroutinebasierte Einzelwertanimation

Animatable ist ein Werthalter, mit dem der Wert animiert werden kann, wenn er über animateTo geändert wird. Dies ist die API, die die Implementierung von animate*AsState unterstützt. Sie sorgt für eine konsistente Fortsetzung und gegenseitige Ausschließlichkeit, d. h., die Wertänderung ist immer kontinuierlich und alle laufenden Animationen werden abgebrochen.

Viele Funktionen von Animatable, einschließlich animateTo, werden als Pausierfunktionen bereitgestellt. Dies bedeutet, dass sie in einen geeigneten Koroutinebereich eingeschlossen werden müssen. Mit dem LaunchedEffect-Komposit können Sie beispielsweise einen Gültigkeitsbereich 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 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 zu Color.Green oder Color.Red animiert. Jede nachfolgende Änderung des booleschen Werts startet die Animation in die andere Farbe. Wenn sich der Wert ändert und eine Animation läuft, wird sie 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 bietet die direkte Verwendung von Animatable in mehreren Aspekten eine genauere Steuerung. Erstens: Animatable kann einen Anfangswert haben, der sich vom ersten Zielwert unterscheidet. Im Codebeispiel oben 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, nämlich snapTo und animateDecay. snapTo legt den aktuellen Wert sofort auf den Zielwert fest. Dies ist nützlich, wenn die Animation selbst nicht die einzige Informationsquelle ist und mit anderen Zuständen wie Touch-Ereignissen synchronisiert werden muss. animateDecay startet eine Animation, die ab der angegebenen Geschwindigkeit langsamer wird. Dies ist nützlich, um das Wischverhalten zu implementieren. Weitere Informationen finden Sie unter Gesten und Animationen.

Standardmäßig werden Animatable Float und Color unterstützt. Es kann jedoch jeder Datentyp verwendet werden, indem ein TwoWayConverter angegeben wird. Weitere Informationen finden Sie unter AnimationVector.

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

Animation: Manuell gesteuerte Animation

Animation ist die Animation API mit der niedrigsten Ebene. Viele der bisher gesehenen Animationen basieren auf der Funktion „Animation“. 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 Lebenszykluskonzept. Es dient als Animationsberechnungsmodul, das von den APIs der höheren Ebene verwendet wird.

TargetBasedAnimation

Die meisten Anwendungsfälle decken andere APIs ab. Wenn Sie TargetBasedAnimation jedoch direkt verwenden, können Sie die Wiedergabedauer der Animation selbst steuern. Im folgenden Beispiel wird die Wiedergabezeit der 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 { 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 keine targetValue angegeben werden. Stattdessen wird die targetValue anhand der Startbedingungen berechnet, die durch initialVelocity und initialValue und die angegebene DecayAnimationSpec festgelegt werden.

Decay-Animationen werden häufig nach einem Flachgeste verwendet, um Elemente bis zum Anschlag zu verlangsamen. Die Animationsgeschwindigkeit beginnt beim von initialVelocityVector festgelegten Wert und wird mit der Zeit langsamer.

Derzeit liegen keine Empfehlungen vor.

Versuchen Sie, sich bei Ihrem Google-Konto .