Krótki przewodnik po animacjach w sekcji Tworzenie

Compose ma wiele wbudowanych mechanizmów animacji, więc trudno jest zdecydować, który z nich wybrać. Poniżej znajdziesz listę typowych zastosowań animacji. Więcej informacji o dostępnych opcjach interfejsu API znajdziesz w pełnej dokumentacji tworzenia animacji.

Animowanie wspólnych właściwości komponentów

Compose udostępnia wygodne interfejsy API, które umożliwiają rozwiązanie wielu typowych problemów związanych z animowaniem. W tej sekcji pokazujemy, jak animować typowe właściwości komponentu.

animacja pojawiania się i znikania,

Zielony element składany, który wyświetla się i ukrywa
Rysunek 1. Animacja pojawiania się i znikania elementu w kolumnie

Aby ukryć lub wyświetlić kompozyt, użyj AnimatedVisibility. Dzieci w AnimatedVisibility mogą używać Modifier.animateEnterExit() do własnych przejść wjazdowych lub wyjazdowych.

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 umożliwiają skonfigurowanie sposobu działania elementu kompozycyjnego podczas jego pojawiania się i znikania. Więcej informacji znajdziesz w pełnej dokumentacji.

Inną opcją animowania widoczności składanego jest animowanie przezroczystości 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 kompozybilny element pozostanie w kompozycji i nadal będzie zajmować wyznaczoną mu przestrzeń. Może to spowodować, że czytniki ekranu i inne mechanizmy ułatwień dostępu nadal będą traktować element jako element na ekranie. Z drugiej strony, AnimatedVisibility ostatecznie usuwa element z kompozycji.

Animowanie wartości alfa komponentu
Rysunek 2. Animowanie kanału alfa kompozytu

Animowanie koloru tła

Element składany z kolorem tła zmieniającym się z czasem w ramach animacji, w której kolory przechodzą jeden w drugi.
Rysunek 3. Animowanie koloru tła komponenta

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

Ta opcja jest skuteczniejsza niż Modifier.background(). Modifier.background() jest dopuszczalne w przypadku jednorazowego ustawienia koloru, ale podczas animacji koloru w czasie może spowodować więcej przekształceń niż to konieczne.

Informacje o animowaniu koloru tła w nieskończoności znajdziesz w sekcji powtarzanie animacji.

Animowanie rozmiaru komponentu

Zielony element składany z animacją zmiany rozmiaru.
Rysunek 4. animacja płynnie przechodząca z mniejszego do większego rozmiaru

W komponowaniu możesz animować rozmiar komponentów na kilka sposobów. Użyj animateContentSize() do animacji między zmianami rozmiaru komponentu.

Jeśli na przykład masz pole z tekstem, który może się rozszerzyć z jednego na kilka 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ć elementu AnimatedContent z elementem SizeTransform, aby opisać, jak powinny wyglądać zmiany rozmiaru.

Animowanie pozycji komponentu

Zielony element składany płynnie przesuwający się w dół i w prawo
Rysunek 5. Składany element przesuwa się o przesunięcie

Aby animować pozycję składanego, użyj atrybutu Modifier.offset{ } w połączeniu z atrybutem 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 podczas animacji pozycji lub rozmiaru komponenty nie będą nakładać się na inne komponenty, użyj właściwości Modifier.layout{ }. Ten modyfikator rozszerza zmiany rozmiaru i położenia na element nadrzędny, który wpływa na inne elementy podrzędne.

Jeśli na przykład przenosisz element Box w elementie Column, a inne elementy muszą się przesunąć, gdy przenosisz element Box, dodaj informacje o przesunięciu do 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, w których drugie pole animuje pozycję X,Y, a trzecie pole reaguje na to, przesuwając się o wartość Y.
Rysunek 6. Tworzenie animacji za pomocą Modifier.layout{ }

Animowanie wypełniania kompozytu

Zielony element kompozytowy zmniejsza się i powiększa po kliknięciu, a wypełnienie jest animowane
Rysunek 7. Kompozycja z animacją wypełnienia

Aby animować wypełnienie kompozytowego, użyj atrybutu animateDpAsState w połączeniu z atrybutem 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 komponentu

Rysunek 8. Animacja wzniesienia w komponowalnym na kliknięcie

Aby animować wysokość komponenta, użyj właściwości animateDpAsState w połączeniu z właściwością Modifier.graphicsLayer{ }. W przypadku jednorazowych zmian wysokości użyj opcji Modifier.shadow(). Jeśli chcesz animować cień, użyj modyfikatora Modifier.graphicsLayer{ }, ponieważ zapewnia on większą wydajność.

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

Animowanie skali, przesunięcia lub obrotu tekstu

Tekst do użycia w kompozycji
Rysunek 9. Tekst płynnie zmieniający rozmiar

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

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
Rysunek 10. Przykład animacji koloru tekstu

Aby animować kolor tekstu, użyj funkcji color w komponowalnym elemencie 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

Tekst na zielonym ekranie
Rysunek 11. Używanie komponentu AnimatedContent do animowania zmian między różnymi komponentami (zwolnione)

Użyj AnimatedContent, aby animować przejście między różnymi komponentami. Jeśli chcesz tylko standardowego znikania komponentów, 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ły różne rodzaje 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.

animacje podczas nawigacji do różnych miejsc docelowych.

Dwa elementy kompozytowe: zielony z napisem Landing (Strona docelowa) i niebieski z napisem Detail (Szczegóły). Animacja polega na przesunięciu elementu Szczegóły nad elementem Strona docelowa.
Rysunek 12. Animowanie między składanymi elementami za pomocą funkcji navigation-compose

Aby animować przejścia między składanymi, gdy używasz artefaktu navigation-compose, określ enterTransitionexitTransition w składanym. 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 rodzajów przejść, które można stosować do treści wchodzących i wychodzących. Więcej informacji znajdziesz w dokumentacji.

Powtarzanie animacji

Zielone tło, które przechodzi w niebieskie, w nieskończoność przez animację między tymi dwoma kolorami.
Rysunek 13. Kolor tła animowany między 2 wartościami, bez końca

Aby animacja była powtarzana w nieskończoność, użyj rememberInfiniteTransitioninfiniteRepeatable animationSpec. Zmień RepeatModes, aby określić, jak ma się przemieszczać.

Użyj finiteRepeatable, 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
}

Rozpoczęcie animacji podczas uruchamiania komponentu

LaunchedEffect jest wykonywane, gdy element kompozycyjny wchodzi do kompozycji. Rozpoczyna animację po uruchomieniu komponentu. Możesz użyć tego do wywołania animacji zmiany stanu. Użycie Animatable z metodą animateTo, aby rozpocząć animację podczas uruchamiania:

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

Tworzenie animacji sekwencyjnych

4 koła z zielonymi strzałkami, które pojawiają się jeden po drugim.
Rysunek 14. Diagram pokazujący, jak sekwencyjna animacja jest odtwarzana jeden po drugim.

Aby wykonywać animacje sekwencyjne lub równoległe, użyj interfejsów API Animatable. Wywoływanie funkcji animateTo w przypadku Animatable powoduje, że każda animacja czeka na zakończenie poprzednich animacji, zanim przejdą dalej . Dzieje się tak, ponieważ jest to funkcja zawieszania.

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

Trzy koła z zielonymi strzałkami, które animują się w tym samym czasie.
Rysunek 15. Diagram pokazujący, jak przebiegają równoległe animacje.

Aby uzyskać animacje działające jednocześnie, użyj interfejsów coroutine API (Animatable#animateTo() lub animate) albo interfejsu Transition API. Jeśli w kontekście coroutine używasz wielu funkcji launch, animacje są uruchamiane 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 za pomocą tego samego stanu sterować jednocześnie wieloma animacjami w różnych usługach. W przykładzie poniżej 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
    }
}

Optymalizacja działania animacji

Animacje w edytorze mogą powodować problemy z wydajnością. Wynika to z charakteru animacji: przesuwanie lub zmiana pikseli na ekranie w szybkim tempie, z klatka na klatkę, aby stworzyć iluzję ruchu.

Zastanów się nad różnymi fazami tworzenia kompozycji: kompozycji, układu i rysowania. Jeśli animacja zmienia fazę układu, wymaga ponownego rozmieszczenia i narysowania wszystkich dotkniętych kompozytowych komponentów. Jeśli animacja występuje w fazie rysowania, domyślnie będzie działać wydajniej niż w fazie układu, ponieważ będzie miała mniej pracy do wykonania.

Aby zapewnić jak najmniejszą aktywność aplikacji podczas animacji, w miarę możliwości wybieraj wersję lambda funkcji Modifier. Spowoduje to pominięcie ponownego tworzenia kompozycji i wykonanie animacji poza fazą tworzenia kompozycji. W przeciwnym razie użyj Modifier.graphicsLayer{ }, ponieważ ten modyfikator zawsze działa w fazie rysowania. Więcej informacji znajdziesz w sekcji odkładanie odczytów w dokumentacji dotyczącej wydajności.

Zmienianie czasu animacji

Domyślnie kompozytor używa w większości przypadków animacji sprężynowej. Sprężyny lub animacje oparte na fizyce wydają się bardziej naturalne. Mogą być też przerywane, ponieważ uwzględniają bieżącą prędkość obiektu zamiast stałego czasu. Jeśli chcesz zastąpić ustawienie domyślne, wszystkie interfejsy API animacji opisane powyżej umożliwiają ustawienie animationSpec, aby dostosować sposób działania animacji, np. czy ma być wykonywana przez określony czas czy bardziej dynamiczna.

Oto 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 odczucie animacji.
  • tween (skrót od between): animacja oparta na czasie trwania, która animuje przejście między 2 wartościami za pomocą funkcji Easing.
  • keyframes: specyfikacja określająca wartości w określonych kluczowych punktach animacji.
  • repeatable: specyfikacja oparta na czasie, która jest odtwarzana określoną liczbę razy, określoną przez RepeatMode.
  • infiniteRepeatable: specyfikacja oparta na czasie, która działa bez końca.
  • snap: natychmiastowe przejście do wartości końcowej bez animacji.
Wpisz tekst alternatywny
Rysunek 16. Brak zestawu specyfikacji a zestaw specyfikacji niestandardowej wiosny

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

Dodatkowe materiały

Więcej przykładów zabawnych animacji w Compose: