Przejdź do treści głównej

Używanie sterownika natywnego dla Animated

· 6 minut czytania
Janic Duplessis
Inżynier oprogramowania w App & Flow
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 →

Przez ostatni rok pracowaliśmy nad poprawą wydajności animacji wykorzystujących bibliotekę Animated. Animacje są kluczowe dla stworzenia pięknego doświadczenia użytkownika, ale mogą być trudne do poprawnego zaimplementowania. Chcemy ułatwić developerom tworzenie wydajnych animacji bez obaw, że fragmenty ich kodu spowodują opóźnienia.

Co to jest?

API Animated zostało zaprojektowane z ważnym ograniczeniem: jest serializowalne. Oznacza to, że możemy przesłać wszystkie informacje o animacji do warstwy natywnej jeszcze przed jej rozpoczęciem, co pozwala natywnemu kodowi wykonywać animację na wątku UI bez konieczności przechodzenia przez mostek w każdej klatce. To niezwykle przydatne, ponieważ po rozpoczęciu animacji wątek JS może zostać zablokowany, a animacja nadal będzie płynnie działać. W praktyce zdarza się to często, ponieważ kod użytkownika działa na wątku JS, a renderowania Reacta również mogą blokować JS na długi czas.

Trochę historii...

Projekt rozpoczął się około roku temu, gdy Expo zbudowało aplikację li.st na Androida. Krzysztof Magiera został zatrudniony do stworzenia początkowej implementacji na Androida. Zakończyło się to sukcesem i li.st była pierwszą aplikacją korzystającą z animacji sterowanych natywnie za pomocą Animated. Kilka miesięcy później Brandon Withrow zbudował początkową implementację na iOS. Następnie Ryan Gomba i ja pracowaliśmy nad dodaniem brakujących funkcji, takich jak obsługa Animated.event, oraz naprawą błędów wykrytych podczas używania tej technologii w aplikacjach produkcyjnych. To był prawdziwie wspólnotowy wysiłek i chciałbym podziękować wszystkim zaangażowanym oraz Expo za sponsorowanie znacznej części rozwoju. Obecnie technologia jest używana przez komponenty Touchable w React Native oraz animacje nawigacyjne w nowo wydanej bibliotece React Navigation.

Jak to działa?

Najpierw przyjrzyjmy się, jak obecnie działają animacje z użyciem Animated ze sterownikiem JS. Podczas korzystania z Animated deklarujesz graf węzłów reprezentujących animacje, które chcesz wykonać, a następnie używasz sterownika do aktualizacji wartości Animated za pomocą predefiniowanej krzywej. Możesz również aktualizować wartość Animated, łącząc ją ze zdarzeniem View przy użyciu Animated.event.

Oto podział kroków animacji i miejsc ich wykonywania:

  • JS: Sterownik animacji używa requestAnimationFrame do wykonywania w każdej klatce i aktualizuje sterowaną wartość przy użyciu nowej wartości obliczonej na podstawie krzywej animacji.

  • JS: Obliczane są wartości pośrednie i przekazywane do węzła właściwości dołączonego do View.

  • JS: View jest aktualizowany za pomocą setNativeProps.

  • Mostek JS do Native.

  • Natywny: Aktualizowany jest UIView lub android.View.

Jak widać, większość pracy odbywa się na wątku JS. Jeśli zostanie on zablokowany, animacja będzie pomijać klatki. Dodatkowo w każdej klatce musi przechodzić przez mostek JS-Native, aby aktualizować widoki natywne.

Sterownik natywny przenosi wszystkie te kroki do warstwy natywnej. Ponieważ Animated generuje graf animowanych węzłów, może być on zserializowany i wysłany do natywnego kodu tylko raz przy rozpoczęciu animacji, eliminując potrzebę wywołań zwrotnych do wątku JS. Kod natywny może samodzielnie aktualizować widoki bezpośrednio na wątku UI w każdej klatce.

Oto przykład, jak możemy serializować wartość animowaną i węzeł interpolacji (nie jest to dokładna implementacja, tylko przykład).

Utwórz natywny węzeł wartości - to wartość, która będzie animowana:

NativeAnimatedModule.createNode({
id: 1,
type: 'value',
initialValue: 0,
});

Utwórz natywny węzeł interpolacji – informuje sterownik natywny, jak interpolować wartość:

NativeAnimatedModule.createNode({
id: 2,
type: 'interpolation',
inputRange: [0, 10],
outputRange: [10, 0],
extrapolate: 'clamp',
});

Utwórz natywny węzeł właściwości – informuje sterownik natywny, do której właściwości widoku jest podpięty:

NativeAnimatedModule.createNode({
id: 3,
type: 'props',
properties: ['style.opacity'],
});

Połącz węzły ze sobą:

NativeAnimatedModule.connectNodes(1, 2);
NativeAnimatedModule.connectNodes(2, 3);

Podłącz węzeł właściwości do widoku:

NativeAnimatedModule.connectToView(3, ReactNative.findNodeHandle(viewRef));

Dzięki temu natywny moduł animacji ma wszystkie informacje potrzebne do bezpośredniej aktualizacji widoków natywnych bez konieczności odwoływania się do JS w celu obliczenia wartości.

Pozostaje tylko uruchomić animację, określając typ krzywej animacyjnej i wartość animowaną do aktualizacji. Animacje czasowe można uprościć, obliczając wcześniej w JS każdą klatkę animacji, aby zmniejszyć implementację natywną.

NativeAnimatedModule.startAnimation({
type: 'timing',
frames: [0, 0.1, 0.2, 0.4, 0.65, ...],
animatedValueId: 1,
});

A oto szczegółowy opis działania animacji:

  • Natywny: Sterownik animacji natywnej używa CADisplayLink lub android.view.Choreographer, aby wykonywać się przy każdej klatce i aktualizować sterowaną wartość przy użyciu nowej wartości obliczonej na podstawie krzywej animacji.

  • Natywny: Obliczane są wartości pośrednie i przekazywane do węzła właściwości podpiętego do widoku natywnego.

  • Natywny: Aktualizowany jest UIView lub android.View.

Jak widać, brak wątku JS i brak mostu – to oznacza szybsze animacje! 🎉🎉

Jak użyć tego w mojej aplikacji?

W przypadku standardowych animacji odpowiedź jest prosta: wystarczy dodać useNativeDriver: true do konfiguracji animacji przy jej uruchamianiu.

Przed:

Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
}).start();

Po:

Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: true, // <-- Add this
}).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.

Działa to również z Animated.event, co jest szczególnie przydatne przy animacjach śledzących pozycję przewijania – bez sterownika natywnego zawsze działałyby z opóźnieniem jednej klatki względem gestu z powodu asynchronicznej natury React Native.

Przed:

<ScrollView
scrollEventThrottle={16}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }]
)}
>
{content}
</ScrollView>

Po:

<Animated.ScrollView // <-- Use the Animated ScrollView wrapper
scrollEventThrottle={1} // <-- Use 1 here to make sure no events are ever missed
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }],
{ useNativeDriver: true } // <-- Add this
)}
>
{content}
</Animated.ScrollView>

Ograniczenia

Nie wszystkie funkcje Animated są obecnie obsługiwane w Native Animated. Główne ograniczenie: można animować tylko właściwości niezwiązane z układem – działają np. transform i opacity, ale właściwości Flexbox i position już nie. Kolejne: Animated.event działa tylko z bezpośrednimi zdarzeniami, nie z propagującymi. Oznacza to, że nie współpracuje z PanResponder, ale działa np. z ScrollView#onScroll.

Native Animated jest częścią React Native od dłuższego czasu, ale nie był dokumentowany, ponieważ uznawano go za eksperymentalny. Upewnij się więc, że używasz aktualnej wersji React Native (0.40+), jeśli chcesz z tej funkcji korzystać.

Zasoby

Więcej informacji o animacjach: polecam ten wykład Christophera Chedeau.

Jeśli chcesz dogłębnie zrozumieć animacje i jak przenoszenie ich do natywnego kodu poprawia UX, obejrzyj też ten wykład Krzysztofa Magiery.