Krótki przewodnik po animacjach w sekcji Tworzenie

Compose ma wiele wbudowanych mechanizmów animacji i może być trudno zdecydować, który z nich wybrać. Poniżej znajdziesz listę typowych zastosowań animacji. Więcej informacji o pełnym zestawie różnych dostępnych opcji interfejsu API znajdziesz w dokumentacji animacji w Compose.

Animowanie typowych właściwości funkcji kompozycyjnych

Compose udostępnia wygodne interfejsy API, które pozwalają rozwiązać wiele typowych problemów związanych z animacjami. W tej sekcji dowiesz się, jak animować typowe właściwości funkcji kompozycyjnej.

Animuj pojawianie się i znikanie

Zielony element kompozycyjny, który się wyświetla i ukrywa
Rysunek 1. Animowanie pojawiania się i znikania elementu w kolumnie

Użyj AnimatedVisibility, aby ukryć lub wyświetlić element kompozycyjny. Elementy podrzędne wewnątrz AnimatedVisibility mogą używać Modifier.animateEnterExit() do własnego przejścia wejścia lub wyjścia.

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
    // ...
}

Parametry wejścia i wyjścia AnimatedVisibility pozwalają skonfigurować zachowanie elementu kompozycyjnego, gdy się pojawia i znika. Więcej informacji znajdziesz w pełnej dokumentacji.

Inną opcją animowania widoczności funkcji kompozycyjnej jest animowanie kanału alfa w czasie za pomocą animateFloatAsState:

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

Zmiana wartości alfa wiąże się jednak z tym, że element kompozycyjny pozostaje w kompozycji i nadal zajmuje miejsce, w którym jest rozmieszczony. Może to spowodować, że czytniki ekranu i inne mechanizmy ułatwień dostępu będą nadal uważać, że element jest na ekranie. Z kolei AnimatedVisibility ostatecznie usuwa element z kompozycji.

Animowanie kanału alfa elementu kompozycyjnego
Rysunek 2. Animowanie przezroczystości elementu kompozycyjnego

Animuj kolor tła

Komponent, którego kolor tła zmienia się w czasie jako animacja, w której kolory przechodzą jeden w drugi.
Rysunek 3. Animowanie koloru tła funkcji kompozycyjnej

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

Ta opcja jest bardziej wydajna niż użycie Modifier.background(). Modifier.background() jest akceptowalny w przypadku jednorazowego ustawienia koloru, ale podczas animowania koloru w czasie może to spowodować więcej rekompozycji niż jest to konieczne.

Informacje o nieskończonej animacji koloru tła znajdziesz w sekcji dotyczącej powtarzania animacji.

Animowanie rozmiaru elementu kompozycyjnego

Zielony element kompozycyjny płynnie zmieniający rozmiar.
Rysunek 4. Element kompozycyjny płynnie animujący przejście między małym a dużym rozmiarem

Compose umożliwia animowanie rozmiaru komponentów na kilka różnych sposobów. Używaj animateContentSize() do animacji między zmianami rozmiaru funkcji kompozycyjnej.

Jeśli na przykład masz pole zawierające tekst, który może się rozszerzać z jednego do kilku wierszy, możesz użyć Modifier.animateContentSize(), aby uzyskać płynniejsze przejście:

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
        }

) {
}

Możesz też użyć AnimatedContent z SizeTransform, aby opisać, jak powinny się zmieniać rozmiary.

Animowanie pozycji elementu kompozycyjnego

Zielony element kompozycyjny płynnie animowany w dół i w prawo
Rysunek 5. Element kompozycyjny przesuwany o przesunięcie

Aby animować pozycję funkcji kompozycyjnej, użyj Modifier.offset{ } w połączeniu z 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
        }
)

Jeśli chcesz mieć pewność, że elementy kompozycyjne nie będą rysowane nad innymi elementami kompozycyjnymi ani pod nimi podczas animowania pozycji lub rozmiaru, użyj Modifier.layout{ }. Ten modyfikator przekazuje zmiany rozmiaru i pozycji do elementu nadrzędnego, co wpływa na inne elementy podrzędne.

Jeśli na przykład przenosisz element Box w elemencie Column, a inne elementy podrzędne muszą zostać przeniesione, gdy przenoszony jest element Box, dołącz informacje o przesunięciu za pomocą elementu Modifier.layout{ } w ten sposób:

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 pola, przy czym drugie pole animuje swoją pozycję X, Y, a trzecie pole reaguje, przesuwając się o wartość Y.
Rysunek 6. Animowanie za pomocą Modifier.layout{ }

Animowanie dopełnienia elementu kompozycyjnego

Zielony element kompozycyjny, który po kliknięciu zmniejsza się i powiększa, z animowanym dopełnieniem
Rysunek 7. Composable z animacją dopełnienia

Aby animować dopełnienie funkcji kompozycyjnej, użyj animateDpAsState w połączeniu z 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
        }
)

Animowanie wysokości elementu kompozycyjnego

Rysunek 8. Animacja wysokości elementu kompozycyjnego po kliknięciu

Aby animować podniesienie elementu kompozycyjnego, użyj animateDpAsState w połączeniu z Modifier.graphicsLayer{ }. W przypadku jednorazowych zmian wysokości użyj Modifier.shadow(). Jeśli animujesz cień, bardziej wydajną opcją jest użycie modyfikatora Modifier.graphicsLayer{ }.

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

Możesz też użyć funkcji kompozycyjnej Card i ustawić właściwość wysokości na różne wartości w zależności od stanu.

Animowanie skali, przesunięcia lub obrotu tekstu

Komponent Text z napisem „Hello” animowany między małym a dużym rozmiarem.
Rysunek 9. Tekst płynnie zmieniający rozmiar

Podczas animowania skali, przesunięcia lub obrotu tekstu ustaw parametr textMotion w TextStyle na TextMotion.Animated. Dzięki temu przejścia między animacjami tekstu będą płynniejsze. Użyj Modifier.graphicsLayer{ }, aby przesunąć, obrócić lub przeskalować tekst.

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

Animowanie koloru tekstu

Napis „Hello Compose” zmieniający kolor z zielonego na niebieski
Rysunek 10. Przykład animacji koloru tekstu

Aby animować kolor tekstu, użyj funkcji lambda color w komponencie 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
    },
    // ...
)

Przełączanie się między różnymi typami treści

Zielony ekran z napisem „Wczytuję”, niebieski ekran z napisem „Wczytano” i biały ekran z napisem „Błąd”. Różne elementy kompozycyjne są przełączane za pomocą prostej animacji.
Rysunek 11. Używanie AnimatedContent do animowania zmian między różnymi elementami kompozycyjnymi (w zwolnionym tempie)

Użyj AnimatedContent, aby animować przejścia między różnymi elementami kompozycyjnymi. Jeśli chcesz tylko standardowego zanikania między elementami kompozycyjnymi, użyj 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 można dostosować, aby wyświetlać wiele różnych rodzajów przejść wejścia i wyjścia. Więcej informacji znajdziesz w dokumentacji dotyczącej AnimatedContent lub w tym poście na blogu na temat AnimatedContent.

Animacja podczas przechodzenia do różnych miejsc docelowych

Animacja 2 funkcji kompozycyjnych: zielonej z napisem „Landing” i niebieskiej z napisem „Detail”. Funkcja kompozycyjna „Detail” przesuwa się nad funkcją kompozycyjną „Landing”.
Rysunek 12. Animowanie między elementami kompozycyjnymi za pomocą navigation-compose

Aby animować przejścia między elementami kompozycyjnymi podczas korzystania z artefaktu navigation-compose, określ enterTransition i exitTransition w elemencie kompozycyjnym. Możesz też ustawić domyślną animację, która będzie używana we wszystkich miejscach docelowych na najwyższym poziomie NavHost:

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(
            // ...
        )
    }
}

Istnieje wiele różnych rodzajów przejść wejścia i wyjścia, które stosują różne efekty do treści przychodzących i wychodzących. Więcej informacji znajdziesz w dokumentacji.

Powtarzanie animacji

Zielone tło, które zmienia się w niebieskie, i tak w nieskończoność.
Rysunek 13. Kolor tła animowany między 2 wartościami, w nieskończoność

Użyj rememberInfiniteTransition z infiniteRepeatable animationSpec, aby animacja powtarzała się w nieskończoność. Zmień RepeatMode, aby określić, jak ma się poruszać.

Użyj repeatable, aby powtórzyć określoną liczbę razy.

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
}

Uruchamianie animacji po uruchomieniu funkcji kompozycyjnej

LaunchedEffect jest uruchamiany, gdy element kompozycyjny wchodzi do kompozycji. Uruchamia animację po uruchomieniu funkcji kompozycyjnej. Możesz użyć tego do zmiany stanu animacji. Użycie Animatable z metodą animateTo, aby uruchomić animację przy uruchamianiu:

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

Tworzenie animacji sekwencyjnych

4 okręgi z zielonymi strzałkami, które animują się po kolei.
Rysunek 14. Diagram pokazujący, jak przebiega animacja sekwencyjna, krok po kroku.

Używaj interfejsów API współprogramów Animatable do wykonywania animacji sekwencyjnych lub równoczesnych. Wywołanie metody animateTo na obiekcie Animatable powoduje, że każda animacja czeka na zakończenie poprzednich, zanim się rozpocznie. Dzieje się tak, ponieważ jest to funkcja zawieszenia.

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

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

Tworzenie jednoczesnych animacji

3 kółka z zielonymi strzałkami, które animują się jednocześnie.
Rysunek 15. Diagram pokazujący, jak przebiegają jednoczesne animacje.

Aby uzyskać jednoczesne animacje, użyj interfejsów API współprogramów (Animatable#animateTo() lub animate) albo interfejsu API Transition. Jeśli użyjesz wielu funkcji uruchamiania w kontekście współprogramu, animacje zostaną uruchomione jednocześnie:

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

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

Możesz użyć interfejsu API updateTransition, aby używać tego samego stanu do sterowania wieloma różnymi animacjami właściwości jednocześnie. W tym przykładzie animowane są 2 właściwości kontrolowane przez zmianę stanu: rect i 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
    }
}

Optymalizowanie wydajności animacji

Animacje w Compose mogą powodować problemy z wydajnością. Wynika to z charakteru animacji, która polega na szybkim przesuwaniu lub zmienianiu pikseli na ekranie klatka po klatce, aby stworzyć iluzję ruchu.

Weź pod uwagę różne fazy Compose: kompozycję, układ i rysowanie. Jeśli animacja zmienia fazę układu, wymaga ponownego ułożenia i przerysowania wszystkich elementów kompozycyjnych, których dotyczy. Jeśli animacja występuje w fazie rysowania, jest domyślnie bardziej wydajna niż w fazie układu, ponieważ wymaga mniej pracy.

Aby aplikacja wykonywała jak najmniej operacji podczas animacji, w miarę możliwości wybierz wersję lambda elementu Modifier. Dzięki temu pomijana jest rekompozycja, a animacja jest wykonywana poza fazą kompozycji. W przeciwnym razie użyj Modifier.graphicsLayer{ }, ponieważ ten modyfikator zawsze działa w fazie rysowania. Więcej informacji na ten temat znajdziesz w sekcji dotyczącej odraczania odczytów w dokumentacji na temat wydajności.

Zmienianie czasu animacji

W Compose większość animacji jest domyślnie sprężynowa. Sprężyny, czyli animacje oparte na fizyce, są bardziej naturalne. Można je też przerwać, ponieważ uwzględniają bieżącą prędkość obiektu, a nie stały czas. Jeśli chcesz zastąpić ustawienia domyślne, wszystkie pokazane wcześniej interfejsy Animation API umożliwiają ustawienie animationSpec, aby dostosować sposób działania animacji, np. określić, czy ma być wykonywana przez określony czas, czy ma być bardziej dynamiczna.

Poniżej znajdziesz podsumowanie różnych opcji animationSpec:

  • spring: animacja oparta na fizyce, domyślna dla wszystkich animacji. Możesz zmienić sztywność lub współczynnik tłumienia, aby uzyskać inny wygląd i styl animacji.
  • tween (skrót od between): animacja oparta na czasie trwania, animuje między 2 wartościami za pomocą funkcji Easing.
  • keyframes: specyfikacja do określania wartości w określonych kluczowych punktach animacji.
  • repeatable: specyfikacja oparta na czasie trwania, która jest uruchamiana określoną liczbę razy, określoną przez RepeatMode.
  • infiniteRepeatable: specyfikacja oparta na czasie trwania, która działa bez końca.
  • snap: natychmiast przechodzi do wartości końcowej bez animacji.
Dwie animacje pokazujące brak ustawionej specyfikacji i niestandardową specyfikację Spring.
Rysunek 16. No spec set versus Custom Spring spec set

Więcej informacji o animationSpecs znajdziesz w pełnej dokumentacji.

Dodatkowe materiały

Więcej przykładów ciekawych animacji w Compose znajdziesz tutaj: