Kurzanleitung zu Animationen in Compose

Compose bietet viele integrierte Animationsmechanismen und es kann schwierig sein, den richtigen auszuwählen. Im Folgenden finden Sie eine Liste gängiger Anwendungsfälle für Animationen. Ausführlichere Informationen zu allen verfügbaren API-Optionen finden Sie in der vollständigen Dokumentation zu Compose-Animationen.

Allgemeine zusammensetzbare Eigenschaften animieren

Compose bietet praktische APIs, mit denen Sie viele gängige Anwendungsfälle für Animationen lösen können. In diesem Abschnitt wird gezeigt, wie Sie gängige Eigenschaften einer zusammensetzbaren Funktion animieren können.

Ein-/Ausblenden animieren

Grünes Composable, das sich selbst ein- und ausblendet
Abbildung 1. Ein- und Ausblenden eines Elements in einer Spalte animieren

Mit AnimatedVisibility können Sie ein Composable ein- oder ausblenden. Untergeordnete Elemente innerhalb von AnimatedVisibility können Modifier.animateEnterExit() für ihren eigenen Übergang beim Ein- oder Ausblenden verwenden.

var visible by remember {
    mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(visible) {
    // your composable here
    // ...
}

Mit den Parametern „enter“ und „exit“ von AnimatedVisibility können Sie konfigurieren, wie sich eine komponierbare Funktion verhält, wenn sie angezeigt wird und verschwindet. Weitere Informationen finden Sie in der vollständigen Dokumentation.

Eine weitere Möglichkeit, die Sichtbarkeit einer zusammensetzbaren Funktion zu animieren, besteht darin, den Alphawert im Laufe der Zeit mit animateFloatAsState zu animieren:

var visible by remember {
    mutableStateOf(true)
}
val animatedAlpha by animateFloatAsState(
    targetValue = if (visible) 1.0f else 0f,
    label = "alpha"
)
Box(
    modifier = Modifier
        .size(200.dp)
        .graphicsLayer {
            alpha = animatedAlpha
        }
        .clip(RoundedCornerShape(8.dp))
        .background(colorGreen)
        .align(Alignment.TopCenter)
) {
}

Wenn Sie jedoch den Alphawert ändern, bleibt das Compose-Element in der Komposition und belegt weiterhin den Platz, an dem es angeordnet ist. Dies könnte dazu führen, dass Screenreader und andere Bedienungshilfen das Element auf dem Bildschirm weiterhin berücksichtigen. AnimatedVisibility entfernt das Element dagegen schließlich aus der Komposition.

Alpha-Wert einer zusammensetzbaren Funktion animieren
Abbildung 2: Alpha-Wert einer komponierbaren Funktion animieren

Hintergrundfarbe animieren

Composable mit einer Hintergrundfarbe, die sich im Laufe der Zeit als Animation ändert, wobei die Farben ineinander übergehen.
Abbildung 3: Hintergrundfarbe einer zusammensetzbaren Funktion animieren

val animatedColor by animateColorAsState(
    if (animateBackgroundColor) colorGreen else colorBlue,
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(animatedColor)
    }
) {
    // your composable here
}

Diese Option ist leistungsfähiger als die Verwendung von Modifier.background(). Modifier.background() ist für eine einmalige Farbeinstellung akzeptabel, aber wenn eine Farbe im Laufe der Zeit animiert wird, kann dies zu mehr Neuzusammensetzungen als nötig führen.

Informationen zum unendlichen Animieren der Hintergrundfarbe finden Sie im Abschnitt Animation wiederholen.

Größe einer zusammensetzbaren Funktion animieren

Grünes Composable, das seine Größe flüssig ändert.
Abbildung 4: Composable, das flüssig zwischen einer kleinen und einer größeren Größe animiert wird

Mit Compose können Sie die Größe von Composables auf verschiedene Arten animieren. Verwenden Sie animateContentSize() für Animationen zwischen Änderungen der Größe von Compose-Elementen.

Wenn Sie beispielsweise ein Feld mit Text haben, der sich von einer auf mehrere Zeilen erweitern kann, können Sie Modifier.animateContentSize() verwenden, um einen flüssigeren Übergang zu erzielen:

var expanded by remember { mutableStateOf(false) }
Box(
    modifier = Modifier
        .background(colorBlue)
        .animateContentSize()
        .height(if (expanded) 400.dp else 200.dp)
        .fillMaxWidth()
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            expanded = !expanded
        }

) {
}

Sie können auch AnimatedContent mit einem SizeTransform verwenden, um zu beschreiben, wie Größenänderungen erfolgen sollen.

Position des Compose-Elements animieren

Grünes Composable, das sich flüssig nach unten und rechts bewegt
Abbildung 5. Composable, das sich um einen Versatz bewegt

Wenn Sie die Position einer zusammensetzbaren Funktion animieren möchten, verwenden Sie Modifier.offset{ } in Kombination mit animateIntOffsetAsState().

var moved by remember { mutableStateOf(false) }
val pxToMove = with(LocalDensity.current) {
    100.dp.toPx().roundToInt()
}
val offset by animateIntOffsetAsState(
    targetValue = if (moved) {
        IntOffset(pxToMove, pxToMove)
    } else {
        IntOffset.Zero
    },
    label = "offset"
)

Box(
    modifier = Modifier
        .offset {
            offset
        }
        .background(colorBlue)
        .size(100.dp)
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            moved = !moved
        }
)

Wenn Sie sicherstellen möchten, dass Composables beim Animieren von Position oder Größe nicht über oder unter anderen Composables gezeichnet werden, verwenden Sie Modifier.layout{ }. Dieser Modifikator gibt Größen- und Positionsänderungen an das übergeordnete Element weiter, was sich dann auf andere untergeordnete Elemente auswirkt.

Wenn Sie beispielsweise ein Box innerhalb eines Column verschieben und die anderen untergeordneten Elemente verschoben werden müssen, wenn das Box verschoben wird, geben Sie die Offset-Informationen mit Modifier.layout{ } so an:

var toggled by remember {
    mutableStateOf(false)
}
val interactionSource = remember {
    MutableInteractionSource()
}
Column(
    modifier = Modifier
        .padding(16.dp)
        .fillMaxSize()
        .clickable(indication = null, interactionSource = interactionSource) {
            toggled = !toggled
        }
) {
    val offsetTarget = if (toggled) {
        IntOffset(150, 150)
    } else {
        IntOffset.Zero
    }
    val offset = animateIntOffsetAsState(
        targetValue = offsetTarget, label = "offset"
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(colorBlue)
    )
    Box(
        modifier = Modifier
            .layout { measurable, constraints ->
                val offsetValue = if (isLookingAhead) offsetTarget else offset.value
                val placeable = measurable.measure(constraints)
                layout(placeable.width + offsetValue.x, placeable.height + offsetValue.y) {
                    placeable.placeRelative(offsetValue)
                }
            }
            .size(100.dp)
            .background(colorGreen)
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(colorBlue)
    )
}

2 Felder, wobei die X- und Y-Position des zweiten Felds animiert wird und das dritte Feld sich ebenfalls um den Y-Wert bewegt.
Abbildung 6: Animationen mit Modifier.layout{ }

Padding einer zusammensetzbaren Funktion animieren

Grünes Compose-Element, das beim Klicken kleiner und größer wird, wobei das Padding animiert wird
Abbildung 7. Composable mit animiertem Padding

Wenn Sie das Padding einer zusammensetzbaren Funktion animieren möchten, verwenden Sie animateDpAsState in Kombination mit Modifier.padding():

var toggled by remember {
    mutableStateOf(false)
}
val animatedPadding by animateDpAsState(
    if (toggled) {
        0.dp
    } else {
        20.dp
    },
    label = "padding"
)
Box(
    modifier = Modifier
        .aspectRatio(1f)
        .fillMaxSize()
        .padding(animatedPadding)
        .background(Color(0xff53D9A1))
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            toggled = !toggled
        }
)

Höhe einer zusammensetzbaren Funktion animieren

Abbildung 8. Animation der Höhe des Composable bei Klick

Wenn Sie die Höhe einer komponierbaren Funktion animieren möchten, verwenden Sie animateDpAsState in Kombination mit Modifier.graphicsLayer{ }. Verwenden Sie für einmalige Höhenänderungen Modifier.shadow(). Wenn Sie den Schatten animieren, ist die Verwendung des Modifikators Modifier.graphicsLayer{ } die leistungsfähigere Option.

val mutableInteractionSource = remember {
    MutableInteractionSource()
}
val pressed = mutableInteractionSource.collectIsPressedAsState()
val elevation = animateDpAsState(
    targetValue = if (pressed.value) {
        32.dp
    } else {
        8.dp
    },
    label = "elevation"
)
Box(
    modifier = Modifier
        .size(100.dp)
        .align(Alignment.Center)
        .graphicsLayer {
            this.shadowElevation = elevation.value.toPx()
        }
        .clickable(interactionSource = mutableInteractionSource, indication = null) {
        }
        .background(colorGreen)
) {
}

Alternativ können Sie das Composable Card verwenden und die Eigenschaft „elevation“ für jeden Status auf unterschiedliche Werte festlegen.

Textskalierung, ‑verschiebung oder ‑rotation animieren

Text-Composable mit dem Text „Hello“, das zwischen einer kleinen und einer größeren Größe animiert wird.
Abbildung 9. Text, der flüssig zwischen zwei Größen animiert wird

Wenn Sie die Skalierung, Verschiebung oder Drehung von Text animieren, setzen Sie den Parameter textMotion für TextStyle auf TextMotion.Animated. Dies sorgt für flüssigere Übergänge zwischen Textanimationen. Verwenden Sie Modifier.graphicsLayer{ }, um den Text zu verschieben, zu drehen oder zu skalieren.

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val scale by infiniteTransition.animateFloat(
    initialValue = 1f,
    targetValue = 8f,
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "scale"
)
Box(modifier = Modifier.fillMaxSize()) {
    Text(
        text = "Hello",
        modifier = Modifier
            .graphicsLayer {
                scaleX = scale
                scaleY = scale
                transformOrigin = TransformOrigin.Center
            }
            .align(Alignment.Center),
        // Text composable does not take TextMotion as a parameter.
        // Provide it via style argument but make sure that we are copying from current theme
        style = LocalTextStyle.current.copy(textMotion = TextMotion.Animated)
    )
}

Textfarbe animieren

Die Wörter „Hello Compose“ mit einer Animation, die die Farbe zwischen Grün und Blau wechselt
Abbildung 10. Beispiel für die Animation der Textfarbe

Wenn Sie die Textfarbe animieren möchten, verwenden Sie den Lambda-Ausdruck color für die zusammensetzbare Funktion BasicText:

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val animatedColor by infiniteTransition.animateColor(
    initialValue = Color(0xFF60DDAD),
    targetValue = Color(0xFF4285F4),
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "color"
)

BasicText(
    text = "Hello Compose",
    color = {
        animatedColor
    },
    // ...
)

Zwischen verschiedenen Inhaltstypen wechseln

Grüner Bildschirm mit der Meldung „Wird geladen“, blauer Bildschirm mit der Meldung „Geladen“ und weißer Bildschirm mit der Meldung „Fehler“. Die verschiedenen Composables werden mit einer einfachen Animation durchlaufen.
Abbildung 11. Mit AnimatedContent Änderungen zwischen verschiedenen Composables animieren (verlangsamt)

Verwenden Sie AnimatedContent, um zwischen verschiedenen Composables zu animieren. Wenn Sie nur eine Standardüberblendung zwischen Composables wünschen, verwenden Sie Crossfade.

var state by remember {
    mutableStateOf(UiState.Loading)
}
AnimatedContent(
    state,
    transitionSpec = {
        fadeIn(
            animationSpec = tween(3000)
        ) togetherWith fadeOut(animationSpec = tween(3000))
    },
    modifier = Modifier.clickable(
        interactionSource = remember { MutableInteractionSource() },
        indication = null
    ) {
        state = when (state) {
            UiState.Loading -> UiState.Loaded
            UiState.Loaded -> UiState.Error
            UiState.Error -> UiState.Loading
        }
    },
    label = "Animated Content"
) { targetState ->
    when (targetState) {
        UiState.Loading -> {
            LoadingScreen()
        }
        UiState.Loaded -> {
            LoadedScreen()
        }
        UiState.Error -> {
            ErrorScreen()
        }
    }
}

AnimatedContent kann so angepasst werden, dass viele verschiedene Arten von Ein- und Ausstiegsübergängen angezeigt werden. Weitere Informationen finden Sie in der Dokumentation zu AnimatedContent oder in diesem Blogpost zu AnimatedContent.

Beim Navigieren zu verschiedenen Zielen animieren

Zwei Composables, ein grünes mit der Aufschrift „Landing“ und ein blaues mit der Aufschrift „Detail“, die animiert werden, indem das Detail-Composable über das Landing-Composable geschoben wird.
Abbildung 12. Animationen zwischen Composables mit Navigation Compose

Wenn Sie Übergänge zwischen Composables mit dem Artefakt navigation-compose animieren möchten, geben Sie enterTransition und exitTransition für ein Composable an. Sie können auch die Standardanimation festlegen, die für alle Ziele auf der obersten Ebene NavHost verwendet werden soll:

val navController = rememberNavController()
NavHost(
    navController = navController, startDestination = "landing",
    enterTransition = { EnterTransition.None },
    exitTransition = { ExitTransition.None }
) {
    composable("landing") {
        ScreenLanding(
            // ...
        )
    }
    composable(
        "detail/{photoUrl}",
        arguments = listOf(navArgument("photoUrl") { type = NavType.StringType }),
        enterTransition = {
            fadeIn(
                animationSpec = tween(
                    300, easing = LinearEasing
                )
            ) + slideIntoContainer(
                animationSpec = tween(300, easing = EaseIn),
                towards = AnimatedContentTransitionScope.SlideDirection.Start
            )
        },
        exitTransition = {
            fadeOut(
                animationSpec = tween(
                    300, easing = LinearEasing
                )
            ) + slideOutOfContainer(
                animationSpec = tween(300, easing = EaseOut),
                towards = AnimatedContentTransitionScope.SlideDirection.End
            )
        }
    ) { backStackEntry ->
        ScreenDetails(
            // ...
        )
    }
}

Es gibt viele verschiedene Arten von Ein- und Ausstiegsübergängen, die unterschiedliche Effekte auf die eingehenden und ausgehenden Inhalte anwenden. Weitere Informationen finden Sie in der Dokumentation.

Animation wiederholen

Ein grüner Hintergrund, der sich in einer Endlosschleife in einen blauen Hintergrund verwandelt und wieder zurück.
Abbildung 13. Hintergrundfarbe, die unendlich zwischen zwei Werten animiert wird

Verwenden Sie rememberInfiniteTransition mit einem infiniteRepeatable animationSpec, um die Animation kontinuierlich zu wiederholen. Ändern Sie RepeatModes, um anzugeben, wie die Bewegung hin und her erfolgen soll.

Verwenden Sie repeatable, um eine bestimmte Anzahl von Wiederholungen festzulegen.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Green,
    targetValue = Color.Blue,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(color)
    }
) {
    // your composable here
}

Animation beim Start einer zusammensetzbaren Funktion starten

LaunchedEffect wird ausgeführt, wenn eine zusammensetzbare Funktion in die Komposition eintritt. Damit wird beim Start einer zusammensetzbaren Funktion eine Animation gestartet, mit der Sie die Änderung des Animationsstatus steuern können. Verwenden Sie Animatable mit der Methode animateTo, um die Animation beim Start zu starten:

val alphaAnimation = remember {
    Animatable(0f)
}
LaunchedEffect(Unit) {
    alphaAnimation.animateTo(1f)
}
Box(
    modifier = Modifier.graphicsLayer {
        alpha = alphaAnimation.value
    }
)

Sequenzielle Animationen erstellen

Vier Kreise mit grünen Pfeilen, die nacheinander zwischen den Kreisen animiert werden.
Abbildung 14. Diagramm, das zeigt, wie eine sequenzielle Animation nacheinander abläuft.

Verwenden Sie die Animatable-Koroutinen-APIs, um sequenzielle oder gleichzeitige Animationen auszuführen. Wenn Sie animateTo nacheinander für Animatable aufrufen, wird jede Animation erst ausgeführt, wenn die vorherigen Animationen abgeschlossen sind. Das liegt daran, dass es sich um eine Sperrfunktion handelt.

val alphaAnimation = remember { Animatable(0f) }
val yAnimation = remember { Animatable(0f) }

LaunchedEffect("animationKey") {
    alphaAnimation.animateTo(1f)
    yAnimation.animateTo(100f)
    yAnimation.animateTo(500f, animationSpec = tween(100))
}

Gleichzeitige Animationen erstellen

Drei Kreise mit grünen Pfeilen, die nacheinander und dann alle gleichzeitig animiert werden.
Abbildung 15. Diagramm, das zeigt, wie gleichzeitige Animationen alle zur selben Zeit ablaufen.

Verwenden Sie die Coroutine APIs (Animatable#animateTo() oder animate) oder die Transition API, um gleichzeitige Animationen zu erzielen. Wenn du mehrere Startfunktionen in einem Koroutinenkontext verwendest, werden die Animationen gleichzeitig gestartet:

val alphaAnimation = remember { Animatable(0f) }
val yAnimation = remember { Animatable(0f) }

LaunchedEffect("animationKey") {
    launch {
        alphaAnimation.animateTo(1f)
    }
    launch {
        yAnimation.animateTo(100f)
    }
}

Mit der updateTransition API können Sie denselben Status verwenden, um viele verschiedene Attributanimationen gleichzeitig zu steuern. Im folgenden Beispiel werden zwei Attribute animiert, die durch eine Statusänderung gesteuert werden: rect und borderWidth:

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

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

Animationsleistung optimieren

Animationen in Compose können zu Leistungsproblemen führen. Das liegt daran, dass bei einer Animation Pixel auf dem Bildschirm schnell von Frame zu Frame bewegt oder geändert werden, um die Illusion einer Bewegung zu erzeugen.

Berücksichtigen Sie die verschiedenen Phasen von Compose: Komposition, Layout und Zeichnen. Wenn deine Animation die Layoutphase ändert, müssen alle betroffenen Composables neu angeordnet und neu gezeichnet werden. Wenn die Animation in der Zeichenphase erfolgt, ist sie standardmäßig leistungsfähiger, als wenn sie in der Layoutphase ausgeführt wird, da insgesamt weniger Arbeit anfällt.

Damit Ihre App während der Animation so wenig wie möglich tut, wählen Sie nach Möglichkeit die Lambda-Version eines Modifier aus. Dadurch wird die Neuzusammensetzung übersprungen und die Animation außerhalb der Zusammensetzungsphase ausgeführt. Verwenden Sie andernfalls Modifier.graphicsLayer{ }, da dieser Modifikator immer in der Zeichenphase ausgeführt wird. Weitere Informationen hierzu finden Sie im Abschnitt Lesevorgänge zurückstellen in der Leistungsdokumentation.

Timing der Animation ändern

In Compose werden für die meisten Animationen standardmäßig federnde Animationen verwendet. Feder- oder physikbasierte Animationen wirken natürlicher. Sie sind auch unterbrechbar, da sie die aktuelle Geschwindigkeit des Objekts anstelle einer festen Zeit berücksichtigen. Wenn ihr die Standardeinstellung überschreiben möchtet, könnt ihr bei allen zuvor gezeigten Animation APIs eine animationSpec festlegen, um die Ausführung einer Animation anzupassen, z. B. ob sie über eine bestimmte Dauer ausgeführt werden oder mehr springen soll.

Im Folgenden finden Sie eine Zusammenfassung der verschiedenen animationSpec-Optionen:

  • spring: Physikbasierte Animation, die Standardeinstellung für alle Animationen. Sie können die Steifigkeit oder das Dämpfungsverhältnis ändern, um ein anderes Erscheinungsbild der Animation zu erzielen.
  • tween (kurz für between): Dauerbasierte Animation, die mit einer Easing-Funktion zwischen zwei Werten animiert.
  • keyframes: Spezifikation zum Festlegen von Werten an bestimmten Schlüsselpunkten in einer Animation.
  • repeatable: Dauerbasierte Spezifikation, die eine bestimmte Anzahl von Malen ausgeführt wird, die durch RepeatMode angegeben wird.
  • infiniteRepeatable: Dauerbasierte Spezifikation, die unbegrenzt ausgeführt wird.
  • snap: Springt ohne Animation sofort zum Endwert.
Zwei Animationen, die den Unterschied zwischen keiner Spezifikation und einer benutzerdefinierten Spring-Spezifikation zeigen.
Abbildung 16. Keine Spezifikation festgelegt im Vergleich zu benutzerdefinierter Spring-Spezifikation

Weitere Informationen zu animationSpecs finden Sie in der vollständigen Dokumentation.

Zusätzliche Ressourcen

Weitere Beispiele für interessante Animationen in Compose finden Sie hier: