Krótki przewodnik po animacjach w sekcji Tworzenie

Compose ma wiele wbudowanych mechanizmów animacji, więc wybór odpowiedniego może być trudny. Poniżej znajdziesz listę typowych zastosowań animacji. Szczegółowe informacje o pełnym zestawie różnych opcji interfejsu API znajdziesz w pełnej dokumentacji Compose Animation.

Animowanie typowych właściwości komponentów

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

Animowanie pojawiania się i znikania

Komponent w kolorze zielonym, który wyświetla się i ukrywa
Rysunek 1. Animowanie pojawiania się i znikania elementu w kolumnie

Użyj AnimatedVisibility, aby ukryć lub wyświetlić komponent. Dzieci w wieku poniżej AnimatedVisibility lat mogą używać Modifier.animateEnterExit() do własnych przejść podczas wchodzenia i wychodzenia.

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 komponentu jest animowanie wartości alfa w czasie za pomocą funkcji 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 komponent pozostaje w kompozycji i nadal zajmuje miejsce, w którym został umieszczony. Może to spowodować, że czytniki ekranu i inne mechanizmy ułatwień dostępu nadal będą uwzględniać element na ekranie. Z drugiej strony funkcja AnimatedVisibility ostatecznie usuwa element z kompozycji.

Animowanie wartości alfa elementu kompozycyjnego
Rysunek 2. Animowanie wartości alfa elementu kompozycyjnego

Animuj kolor tła

Kompozycja z kolorem tła zmieniającym się z czasem w ramach animacji, w której kolory przenikają się nawzajem.
Rysunek 3. Animowanie koloru tła komponentu

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

Ta opcja jest wydajniejsza niż użycie Modifier.background().Modifier.background() jest dopuszczalny w przypadku jednorazowego ustawienia koloru, ale podczas animowania koloru w czasie może powodować więcej ponownych kompozycji niż jest to konieczne.

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

Animowanie rozmiaru komponentu

Zielony komponent, który płynnie zmienia rozmiar.
Rysunek 4. Kompozycja z płynną animacją przejścia między małym a większym rozmiarem

Compose umożliwia animowanie rozmiaru komponentów na kilka sposobów. Użyj animateContentSize() w przypadku animacji między zmianami rozmiaru komponentu.

Jeśli na przykład masz pole tekstowe, które może się rozszerzać z 1 do kilku wierszy, możesz użyć funkcji 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ć AnimatedContentSizeTransform, aby opisać, jak mają przebiegać zmiany rozmiaru.

Animowanie pozycji komponentu

Zielony komponent płynnie animowany w dół i w prawo
Rysunek 5. Przenoszenie komponentu o określone przesunięcie

Aby animować pozycję komponentu, 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 ani pod innymi elementami kompozycyjnymi podczas animowania pozycji lub rozmiaru, użyj Modifier.layout{ }. Ten modyfikator przekazuje zmiany rozmiaru i pozycji do elementu nadrzędnego, co następnie wpływa na inne elementy podrzędne.

Jeśli na przykład przenosisz Box w ramach Column, a inne elementy podrzędne muszą się przesuwać wraz z Box, uwzględnij informacje o przesunięciu w 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, z których drugie animuje swoją pozycję X,Y, a trzecie reaguje, przesuwając się o wartość Y.
Rysunek 6. Animowanie za pomocą Modifier.layout{ }

Animowanie dopełnienia komponentu

Zielony komponent, który po kliknięciu zmniejsza się i powiększa, a jego dopełnienie jest animowane.
Rysunek 7. Kompozycja z animowanym dopełnieniem

Aby animować dopełnienie komponentu, 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 uniesienia funkcji kompozycyjnej

Rysunek 8. Animowanie wysokości komponentu po kliknięciu

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

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ć komponentu Card i ustawić właściwość elevation na różne wartości w zależności od stanu.

Animowanie skali, przesunięcia lub obrotu tekstu

Kompozycja tekstowa z napisem Hello, która animuje się między małym a większym rozmiarem.
Rysunek 9. Tekst płynnie zmieniający rozmiar

Podczas animowania skalowania, translacji lub obrotu tekstu ustaw parametr textMotion na TextStyle na TextMotion.Animated. Zapewnia to płynniejsze przejścia między animacjami tekstu. Użyj Modifier.graphicsLayer{ }, aby przetłumaczyć, 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

Słowa Hello Compose zmieniające kolor z zielonego na niebieski
Rysunek 10. Przykład animacji koloru tekstu

Aby animować kolor tekstu, użyj lambdy color w funkcji kompozycyjnej 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 między różnymi typami treści

Zielony ekran z napisem „Wczytywanie”, niebieski ekran z napisem „Wczytano” i biały ekran z napisem „Błąd” – między różnymi elementami kompozycyjnymi przechodzi się za pomocą prostej animacji.
Rysunek 11. Używanie AnimatedContent do animowania zmian między różnymi komponentami kompozycyjnymi (zwolnione)

Użyj AnimatedContent, aby animować przejścia między różnymi komponentami, a jeśli chcesz tylko standardowego zanikania między komponentami, 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ć tak, aby wyświetlać wiele różnych rodzajów przejść wejścia i wyjścia. Więcej informacji znajdziesz w dokumentacji na stronie AnimatedContent lub w tym poście na blogu AnimatedContent.

Animowanie podczas nawigacji do różnych miejsc docelowych

Dwa elementy kompozycyjne, jeden zielony z napisem „Landing”, a drugi niebieski z napisem „Detail”, animowane przez przesuwanie elementu kompozycyjnego „Detail” nad elementem kompozycyjnym „Landing”.
Rysunek 12. Animowanie przejść między komponentami za pomocą biblioteki navigation-compose

Aby animować przejścia między komponentami kompozycyjnymi podczas korzystania z artefaktu navigation-compose, określ enterTransitionexitTransition w komponencie 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 wywołują różne efekty na przychodzących i wychodzących treściach. Więcej informacji znajdziesz w dokumentacji.

Powtarzanie animacji

Zielone tło, które zmienia się w niebieskie, z nieskończoną animacją między tymi dwoma kolorami.
Rysunek 13. Animacja koloru tła między dwiema wartościami, nieskończona

Użyj rememberInfiniteTransitioninfiniteRepeatable animationSpec, aby animacja była powtarzana w nieskończoność. Zmień wartość parametru RepeatModes, aby określić, jak ma się poruszać w przód i w tył.

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 komponentu

LaunchedEffect jest uruchamiana, gdy funkcja kompozycyjna wchodzi w skład kompozycji. Uruchamia animację po uruchomieniu funkcji kompozycyjnej. Możesz użyć tego, aby wywołać zmianę stanu animacji. Używanie Animatable z metodą animateTo do uruchamiania animacji po uruchomieniu:

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

Tworzenie animacji sekwencyjnych

Cztery kółka z zielonymi strzałkami animowanymi między nimi, animowane kolejno po sobie.
Rysunek 14. Diagram pokazujący, jak animacja sekwencyjna postępuje krok po kroku.

Używaj interfejsów API Animatable współprogramów do wykonywania animacji sekwencyjnych lub równoczesnych. Wywoływanie funkcji animateTo na obiekcie Animatable jedna po drugiej powoduje, że każda animacja czeka na zakończenie poprzednich animacji przed przejściem do następnej. Dzieje się tak, ponieważ jest to funkcja zawieszająca.

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 równoczesnych animacji

Trzy kółka z zielonymi strzałkami animowanymi do każdego z nich, animowane jednocześnie.
Rysunek 15. Diagram pokazujący, jak przebiegają równoczesne animacje.

Użyj interfejsów API do współprogramów (Animatable#animateTo() lub animate) albo interfejsu Transition API, aby uzyskać równoczesne animacje. Jeśli w kontekście współprogramu użyjesz kilku funkcji uruchamiania, animacje zostaną uruchomione w tym samym czasie:

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 updateTransition API, aby używać tego samego stanu do sterowania wieloma różnymi animacjami właściwości w tym samym czasie. W tym przykładzie animowane są 2 właściwości kontrolowane przez zmianę stanu: rectborderWidth:

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 natury animacji: szybko zmieniających się pikseli na ekranie, klatka po klatce, aby stworzyć iluzję ruchu.

Weź pod uwagę różne fazy funkcji Compose: kompozycję, układ i rysowanie. Jeśli animacja zmienia fazę układu, wymaga ponownego ułożenia i narysowania wszystkich powiązanych komponentów. Jeśli animacja występuje w fazie rysowania, jest domyślnie bardziej wydajna niż w przypadku, gdyby była uruchamiana w fazie układu, ponieważ ma mniej pracy do wykonania.

Aby zminimalizować obciążenie aplikacji podczas animacji, w miarę możliwości wybieraj wersję lambda funkcji Modifier. Pomija to ponowne komponowanie i wykonuje animację poza fazą kompozycji. W przeciwnym razie użyj Modifier.graphicsLayer{ }, ponieważ ten modyfikator zawsze działa w fazie rysowania. Więcej informacji znajdziesz w sekcji odraczanie odczytów w dokumentacji dotyczącej wydajności.

Zmienianie czasu animacji

Domyślnie Compose używa animacji sprężynowych w przypadku większości animacji. Animacje oparte na fizyce, czyli sprężyny, 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 interfejsy API animacji, które zostały zaprezentowane wcześniej, umożliwiają ustawienie animationSpec w celu dostosowania sposobu działania animacji, np. określenia czasu jej trwania lub zwiększenia efektu sprężystości.

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 działanie animacji.
  • tween (skrót od between): animacja oparta na czasie trwania, animuje między dwiema wartościami za pomocą funkcji Easing.
  • keyframes: specyfikacja określająca wartości w określonych kluczowych punktach animacji.
  • repeatable: specyfikacja oparta na czasie trwania, która jest uruchamiana określoną liczbę razy, podaną w parametrze RepeatMode.
  • infiniteRepeatable: specyfikacja oparta na czasie trwania, która działa bezterminowo.
  • snap: natychmiastowe przejście do wartości końcowej bez animacji.
Dwie animacje pokazujące brak zestawu specyfikacji w porównaniu z niestandardowym zestawem specyfikacji sprężyny.
Rysunek 16. Brak zestawu specyfikacji w porównaniu z zestawem specyfikacji wody źródlanej niestandardowej

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

Dodatkowe materiały

Więcej przykładów ciekawych animacji w Compose znajdziesz w tych artykułach: