Przejdź do treści głównej
Wersja: Następna

Animacje

Nieoficjalne Tłumaczenie Beta

Ta strona została przetłumaczona przez PageTurner AI (beta). Nie jest oficjalnie zatwierdzona przez projekt. Znalazłeś błąd? Zgłoś problem →

Animacje są niezwykle istotne dla stworzenia doskonałego doświadczenia użytkownika. Stacjonarne obiekty muszą pokonać inercję, gdy zaczynają się poruszać. Poruszające się obiekty mają pęd i rzadko zatrzymują się natychmiast. Animacje pozwalają przekazać fizycznie wiarygodny ruch w Twoim interfejsie.

React Native dostarcza dwa komplementarne systemy animacji: Animated do precyzyjnej i interaktywnej kontroli konkretnych wartości oraz LayoutAnimation do animowanych globalnych transakcji układu.

API Animated

API Animated zostało zaprojektowane, by w wydajny sposób wyrażać różnorodne wzorce animacji i interakcji. Animated koncentruje się na deklaratywnych relacjach między wejściami i wyjściami, z konfigurowalnymi przekształceniami pomiędzy oraz metodami start/stop do kontroli czasowego wykonywania animacji.

Animated eksportuje sześć animowalnych typów komponentów: View, Text, Image, ScrollView, FlatList i SectionList, ale możesz też tworzyć własne używając Animated.createAnimatedComponent().

Przykładowo, kontener pojawiający się z efektem ściemniania po zamontowaniu może wyglądać tak:

Przeanalizujmy, co się tu dzieje. W metodzie render FadeInView inicjalizowana jest nowa wartość Animated.Value o nazwie fadeAnim za pomocą useRef. Właściwość opacity w View jest powiązana z tą animowaną wartością. W tle wartość numeryczna jest wyodrębniana i używana do ustawienia przezroczystości.

Gdy komponent jest montowany, przezroczystość jest ustawiana na 0. Następnie uruchamiana jest animacja wygładzająca na wartości fadeAnim, która będzie aktualizować wszystkie powiązane mapowania (w tym przypadku tylko opacity) w każdej klatce, gdy wartość animuje się do końcowej wartości 1.

Dzieje się to w sposób zoptymalizowany, szybszy niż wywołanie setState i ponowne renderowanie. Ponieważ cała konfiguracja jest deklaratywna, możliwe będzie wdrożenie dalszych optymalizacji, które serializują konfigurację i uruchamiają animację w wątku o wysokim priorytecie.

Konfigurowanie animacji

Animacje są wysoce konfigurowalne. Niestandardowe i predefiniowane funkcje wygładzania, opóźnienia, czas trwania, współczynniki tłumienia, stałe sprężystości i wiele innych można dostosować w zależności od typu animacji.

Animated dostarcza kilka typów animacji, z których najczęściej używaną jest Animated.timing(). Obsługuje animowanie wartości w czasie przy użyciu jednej z wielu predefiniowanych funkcji wygładzania lub możesz użyć własnej. Funkcje wygładzania są typowo używane w animacjach do przekazywania stopniowego przyspieszania i zwalniania obiektów.

Domyślnie timing używa krzywej easeInOut, która przekazuje stopniowe przyspieszanie do pełnej prędkości i kończy się powolnym wyhamowaniem. Możesz określić inną funkcję wygładzania przekazując parametr easing. Obsługiwane jest także niestandardowe duration lub nawet delay przed rozpoczęciem animacji.

Na przykład, jeśli chcemy stworzyć dwusekundową animację obiektu, który lekko się cofa przed przesunięciem na końcową pozycję:

tsx
Animated.timing(this.state.xPosition, {
toValue: 100,
easing: Easing.back(),
duration: 2000,
useNativeDriver: true,
}).start();

Zobacz sekcję Konfigurowanie animacji w dokumentacji API Animated, aby dowiedzieć się więcej o wszystkich parametrach konfiguracyjnych obsługiwanych przez wbudowane animacje.

Komponowanie animacji

Animacje można łączyć i odtwarzać sekwencyjnie lub równolegle. Animacje sekwencyjne mogą rozpoczynać się bezpośrednio po zakończeniu poprzedniej animacji lub po określonym opóźnieniu. API Animated dostarcza kilka metod, takich jak sequence() i delay(), z których każda przyjmuje tablicę animacji do wykonania i automatycznie wywołuje start()/stop() w razie potrzeby.

Na przykład, następująca animacja najpierw płynnie zatrzymuje się, a następnie odbija sprężyście równocześnie z rotacją:

tsx
Animated.sequence([
// decay, then spring to start and twirl
Animated.decay(position, {
// coast to a stop
velocity: {x: gestureState.vx, y: gestureState.vy}, // velocity from gesture release
deceleration: 0.997,
useNativeDriver: true,
}),
Animated.parallel([
// after decay, in parallel:
Animated.spring(position, {
toValue: {x: 0, y: 0}, // return to start
useNativeDriver: true,
}),
Animated.timing(twirl, {
// and twirl
toValue: 360,
useNativeDriver: true,
}),
]),
]).start(); // start the sequence group

Jeśli jedna animacja zostanie zatrzymana lub przerwana, wszystkie inne animacje w grupie również zostaną zatrzymane. Animated.parallel ma opcję stopTogether, którą można ustawić na false, aby wyłączyć to zachowanie.

Pełną listę metod kompozycyjnych znajdziesz w sekcji Komponowanie animacji dokumentacji API Animated.

Łączenie animowanych wartości

Możesz łączyć dwie animowane wartości za pomocą dodawania, mnożenia, dzielenia lub modulo, aby utworzyć nową animowaną wartość.

W niektórych przypadkach animowana wartość musi odwrócić inną animowaną wartość do obliczeń. Przykładem jest odwrócenie skali (2x → 0.5x):

tsx
const a = new Animated.Value(1);
const b = Animated.divide(1, a);

Animated.spring(a, {
toValue: 2,
useNativeDriver: true,
}).start();

Interpolacja

Każdą właściwość można najpierw poddać interpolacji. Interpolacja mapuje zakresy wejściowe na wyjściowe, zwykle przy użyciu interpolacji liniowej, ale obsługuje również funkcje wygładzania. Domyślnie ekstrapoluje krzywą poza podane zakresy, ale możesz również ustawić ograniczenie wartości wyjściowej.

Podstawowe mapowanie do konwersji zakresu 0-1 na zakres 0-100 wyglądałoby następująco:

tsx
value.interpolate({
inputRange: [0, 1],
outputRange: [0, 100],
});

Na przykład możesz chcieć, aby Twoja Animated.Value zmieniała się od 0 do 1, ale animowała pozycję od 150px do 0px i przezroczystość od 0 do 1. Można to osiągnąć, modyfikując style z poprzedniego przykładu w następujący sposób:

tsx
  style={{
opacity: this.state.fadeAnim, // Binds directly
transform: [{
translateY: this.state.fadeAnim.interpolate({
inputRange: [0, 1],
outputRange: [150, 0] // 0 : 150, 0.5 : 75, 1 : 0
}),
}],
}}

interpolate() obsługuje również wiele segmentów zakresu, co jest przydatne do definiowania martwych stref i innych technik. Na przykład aby uzyskać relację negacji przy -300 przechodzącą do 0 przy -100, następnie do 1 przy 0, potem do 0 przy 100 z martwą strefą pozostającą na 0 dla wartości poza tym zakresem:

tsx
value.interpolate({
inputRange: [-300, -100, 0, 100, 101],
outputRange: [300, 0, 1, 0, 0],
});

Co mapowałoby się następująco:

Input | Output
------|-------
-400| 450
-300| 300
-200| 150
-100| 0
-50| 0.5
0| 1
50| 0.5
100| 0
101| 0
200| 0

interpolate() obsługuje również mapowanie na ciągi znaków, umożliwiając animowanie kolorów oraz wartości z jednostkami. Na przykład aby animować obrót:

tsx
value.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg'],
});

interpolate() obsługuje dowolne funkcje wygładzania, z których wiele jest zaimplementowanych w module Easing. interpolate() również ma konfigurowalne zachowanie dla ekstrapolacji outputRange. Możesz ustawić ekstrapolację przez opcje extrapolate, extrapolateLeft lub extrapolateRight. Domyślnie ustawiona jest wartość extend, ale możesz użyć clamp aby zapobiec przekroczeniu outputRange.

Śledzenie wartości dynamicznych

Animowane wartości mogą śledzić inne wartości poprzez ustawienie toValue animacji na inną animowaną wartość zamiast liczby. Na przykład animację "Chat Heads" (jak w Messengerze na Androidzie) można zaimplementować za pomocą spring() przypiętego do innej animowanej wartości lub timing() z duration 0 dla precyzyjnego śledzenia. Można je również łączyć z interpolacjami:

tsx
Animated.spring(follower, {toValue: leader}).start();
Animated.timing(opacity, {
toValue: pan.x.interpolate({
inputRange: [0, 300],
outputRange: [1, 0],
}),
useNativeDriver: true,
}).start();

Wartości leader i follower implementuje się za pomocą Animated.ValueXY(). ValueXY to wygodny sposób obsługi interakcji 2D jak przesuwanie czy przeciąganie. Jest to opakowanie zawierające dwie instancje Animated.Value i funkcje pomocnicze, dzięki czemu ValueXY często może zastąpić Value. Pozwala śledzić zarówno wartości x, jak i y.

Śledzenie gestów

Gesty (jak przesuwanie czy przewijanie) oraz inne zdarzenia można mapować bezpośrednio na animowane wartości za pomocą Animated.event. Składnia wykorzystuje ustrukturyzowane mapowanie, gdzie pierwszy poziom to tablica umożliwiająca mapowanie wielu argumentów, zawierająca zagnieżdżone obiekty.

Np. przy obsłudze gestów poziomego przewijania, aby zmapować event.nativeEvent.contentOffset.x na scrollX (Animated.Value):

tsx
 onScroll={Animated.event(
// scrollX = e.nativeEvent.contentOffset.x
[{nativeEvent: {
contentOffset: {
x: scrollX
}
}
}]
)}

Poniższy przykład implementuje poziomą karuzelę, gdzie wskaźniki pozycji przewijania są animowane przy użyciu Animated.event z ScrollView:

Przykład ScrollView z animowanym zdarzeniem

Podczas używania PanResponder, możesz zastosować poniższy kod do wyciągnięcia pozycji x i y z gestureState.dx i gestureState.dy. Używamy null w pierwszej pozycji tablicy, ponieważ interesuje nas tylko drugi argument przekazywany do handlera PanResponder, którym jest gestureState.

tsx
onPanResponderMove={Animated.event(
[null, // ignore the native event
// extract dx and dy from gestureState
// like 'pan.x = gestureState.dx, pan.y = gestureState.dy'
{dx: pan.x, dy: pan.y}
])}

Przykład użycia PanResponder z Animated Event

Reagowanie na bieżącą wartość animacji

Możesz zauważyć, że nie ma bezpośredniego sposobu odczytania bieżącej wartości podczas animacji. Wynika to z faktu, że wartość może być znana tylko w natywnym środowisku wykonawczym z powodu optymalizacji. Jeśli potrzebujesz uruchomić JavaScript w odpowiedzi na bieżącą wartość, istnieją dwa podejścia:

  • spring.stopAnimation(callback) zatrzyma animację i wywoła callback z końcową wartością. Jest to przydatne przy tworzeniu przejść gestów.

  • spring.addListener(callback) będzie asynchronicznie wywoływać callback podczas działania animacji, dostarczając aktualną wartość. Jest to przydatne do wyzwalania zmian stanu, np. przyciągania elementu do nowej opcji gdy użytkiek przeciąga go bliżej, ponieważ te większe zmiany stanu są mniej wrażliwe na kilka klatek opóźnienia w porównaniu do ciągłych gestów jak przeciąganie, które muszą działać z 60 klatkami na sekundę.

Animated został zaprojektowany jako w pełni serializowalny, aby animacje mogły działać wysokowydajnie, niezależnie od standardowej pętli zdarzeń JavaScript. Wpływa to na API, więc miej to na uwadze, gdy coś wydaje się trudniejsze w porównaniu z w pełni synchronicznym systemem. Sprawdź Animated.Value.addListener jako sposób na obejście niektórych ograniczeń, ale używaj tej metody oszczędnie, ponieważ może mieć implikacje wydajnościowe w przyszłości.

Korzystanie z natywnego sterownika

API Animated jest zaprojektowane jako serializowalne. Używając natywnego sterownika, wysyłamy wszystkie informacje o animacji do warstwy natywnej przed jej rozpoczęciem, co pozwala natywnemu kodowi wykonać animację w wątku UI bez konieczności przechodzenia przez most na każdej klatce. Po rozpoczęciu animacji, blokada wątku JS nie wpływa na jej działanie.

Użycie natywnego sterownika dla standardowych animacji można osiągnąć przez ustawienie useNativeDriver: true w konfiguracji animacji podczas jej uruchamiania. Animacje bez właściwości useNativeDriver domyślnie będą używać wartości false z powodów historycznych, ale wyemitują ostrzeżenie (oraz błąd typu w TypeScript).

tsx
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: true, // <-- Set this to true
}).start();

Wartości animowane są kompatybilne tylko z jednym sterownikiem, więc jeśli używasz sterownika natywnego przy uruchamianiu animacji na wartości, upewnij się że każda animacja tej wartości również go używa.

Natywny sterownik działa również z Animated.event. Jest to szczególnie przydatne dla animacji śledzących pozycję przewijania, ponieważ bez sterownika natywnego animacja zawsze będzie działać z opóźnieniem jednej klatki względem gestu z powodu asynchronicznej natury React Native.

tsx
<Animated.ScrollView // <-- Use the Animated ScrollView wrapper
onScroll={Animated.event(
[
{
nativeEvent: {
contentOffset: {y: this.state.animatedValue},
},
},
],
{useNativeDriver: true}, // <-- Set this to true
)}>
{content}
</Animated.ScrollView>

Możesz zobaczyć native driver w działaniu, uruchamiając aplikację RNTester, a następnie ładując przykład Native Animated. Możesz także przejrzeć kod źródłowy, aby dowiedzieć się, jak powstały te przykłady.

Ostrzeżenia

Nie wszystko co możesz zrobić z Animated jest obecnie wspierane przez natywny sterownik. Główne ograniczenie to możliwość animowania tylko właściwości nielayoutowych: właściwości jak transform i opacity będą działać, ale właściwości Flexbox i pozycji już nie. Przy użyciu Animated.event, działa to tylko z bezpośrednimi zdarzeniami, nie z bąbelkującymi. Oznacza to że nie działa z PanResponder, ale działa z elementami jak ScrollView#onScroll.

Podczas działania animacji, może ona blokować renderowanie dodatkowych wierszy w komponentach VirtualizedList. Jeśli potrzebujesz uruchomić długą lub zapętlającą się animację podczas gdy użytkownik przewija listę, możesz użyć isInteraction: false w konfiguracji animacji, aby zapobiec temu problemowi.

Warto pamiętać

Podczas używania stylów transformacji takich jak rotateY, rotateX i innych, upewnij się, że zastosowano styl transformacji perspective. Obecnie niektóre animacje mogą nie renderować się na Androidzie bez tego. Przykład poniżej.

tsx
<Animated.View
style={{
transform: [
{scale: this.state.scale},
{rotateY: this.state.rotateY},
{perspective: 1000}, // without this line this Animation will not render on Android while working fine on iOS
],
}}
/>

Dodatkowe przykłady

Aplikacja RNTester zawiera różne przykłady użycia Animated:

API LayoutAnimation

LayoutAnimation pozwala globalnie skonfigurować animacje create i update, które zostaną zastosowane do wszystkich widoków w kolejnym cyklu renderowania/layoutu. Jest to szczególnie przydatne przy aktualizacjach układu Flexbox, gdy nie chcemy ręcznie mierzyć lub obliczać właściwości do bezpośredniej animacji. Sprawdza się zwłaszcza gdy zmiany układu wpływają na elementy nadrzędne, np. rozwijany element "zobacz więcej", który zwiększa rozmiar rodzica i przesuwa w dół kolejny wiersz – w przeciwnym razie wymagałoby to jawnej koordynacji między komponentami do synchronicznej animacji.

Pamiętaj, że choć LayoutAnimation jest potężne i użyteczne, oferuje znacznie mniejszą kontrolę niż Animated i inne biblioteki animacji. Jeśli nie uda Ci się osiągnąć pożądanych efektów z LayoutAnimation, rozważ inne podejście.

Aby działało na Androidzie, ustaw następujące flagi poprzez UIManager:

tsx
UIManager.setLayoutAnimationEnabledExperimental(true);

Ten przykład używa predefiniowanej wartości. Możesz dostosować animacje według potrzeb – więcej informacji znajdziesz w LayoutAnimation.js.

Dodatkowe uwagi

requestAnimationFrame

requestAnimationFrame to znane z przeglądarek polyfill. Przyjmuje funkcję jako argument i wywołuje ją przed następnym odświeżeniem ekranu. To podstawowy budulec animacji opartych na JavaScripcie. Zazwyczaj nie musisz wywoływać tego bezpośrednio – API animacji zarządza aktualizacjami klatek za ciebie.

setNativeProps

Jak wspomniano w sekcji o bezpośredniej manipulacji, setNativeProps pozwala modyfikować właściwości natywnych komponentów (fizycznie istniejących w warstwie natywnej) bez użycia setState i ponownego renderowania hierarchii komponentów.

Możemy tego użyć np. w animacjach sprężynowych do aktualizacji skali – szczególnie przydatne gdy modyfikowany komponent jest głęboko zagnieżdżony i nie został zoptymalizowany za pomocą shouldComponentUpdate.

Jeśli twoje animacje tracą klatki (poniżej 60 FPS), rozważ:

  1. Użycie setNativeProps lub optymalizację przez shouldComponentUpdate
  2. Uruchamianie animacji na wątku UI zamiast JavaScript (opcja useNativeDriver)
  3. Odłożenie wymagających obliczeniowo zadań na koniec animacji za pomocą InteractionManager

Kluczową metrykę monitorujesz w Dev Menu przez narzędzie "FPS Monitor".