React Native – podstawowe animacje

Odpowiednio zastosowane mikroanimacje tworzone w React Native mogą pozytywnie wpływać na interakcje użytkownika z naszą aplikacją. Nie tylko uatrakcyjniają jej wygląd i sprawiają, że używanie jej jest bardziej ciekawe, ale również zapewniają użytkownika o tym, że jego działania faktycznie mają wpływ na aplikacje (np. loadery).

Dla początkujących programistów React Native temat animacji może wydać się skomplikowany. Mnogość opcji w postaci takich bibliotek jak Reanimated, Animatable czy Lottie React Native, może przytłaczać, a kod animacji pisanych z podstawową biblioteką Animated może wyglądać dość nieprzyjaźnie.

Nic bardziej mylnego. Postaram się pokazać, że z użyciem biblioteki Animated można szybko tworzyć proste animacje znacznie poprawiające wygląd aplikacji. 

Artykuł jest skierowany do osób, które mają podstawową wiedzę na temat tego w jaki sposób tworzyć  komponenty w RN.

Checkbox - animowanie wielkości

Zacznijmy od najprostszego możliwego przykładu.

Stwórzmy w animację sprowadzającą się do zmiany wielkości wewnętrznego kwadratu w reakcji na tapnięcie ekranu przez użytkownika.

Przykładowe animacje | Zmiana wielkości wewnętrznego kwadratu
Przykładowe animacje | Zmiana wielkości wewnętrznego kwadratu
Krok 1

Implementacja

Zabierzmy się za implementację.

1. Najpierw stwórzmy komponent bez animacji.


type Props = {
    value: boolean;
    onPress: (value: boolean) =>; void;
};
 

const Checkbox: React.FC<Props> = ({ onPress, value }) => {
    return (
        <TouchableOpacity
            style={styles.container}
            onPress={() => {
                onPress(!value);
        }}>
            {value && <View style={[styles.insideBox]} />}
        </TouchableOpacity>
    );
};

export default Checkbox;
 

Do komponentu przekazujemy dwa propsy:

  • value, przechowujący informacje o tym czy checkbox jest zaznaczony, czy odznaczony,
  • oraz funkcję onPress umożliwiającą zmianę wartości value podczas naciśnięcia na checkbox.

Komponent składa się z TouchableOpacity oraz View reprezentującego prostokąt w środku checkboxa, który renderujemy warunkowo tylko gdy wartość checkboxa wynosi true. 

2. Teraz zajmijmy się nadaniem odpowiednich stylów.


const styles = StyleSheet.create({
    container: {
        height: 120,
        width: 120,
        borderColor: theme.colors.black,
        backgroundColor: theme.colors.background,
        borderWidth: 0.5,
        borderRadius: 10,
        alignItems: "center",
        justifyContent: "center",
    },
 
    insideBox: {
        backgroundColor: theme.colors.black,
        height: 85,
        width: 85,
        borderRadius: 10,
    },
});

Najważniejsze jest aby nadać TouchableOpacity większą szerokość i wysokość niż wewnętrznemu View.

Za ułożenie View centralnie na środku TouchableOpacity odpowiadają:

alignItems:"center"

oraz

justifyContent:"center"

Nie zapomnijmy o nadaniu wartości border w TouchableOpacity oraz koloru tła dla View.

Na tym etapie komponent powinien wyglądać mniej więcej tak:

Zmiana wielkości wewnętrznego kwadratu | krok 1
Zmiana wielkości wewnętrznego kwadratu | krok 1
Krok 2

Animacja

Nuda… prawda? Pora na dodanie animacji!

1. Na początku zaimportujmy dostarczaną przez React Native bibliotekę Animated.


import { Animated } from "react-native";

Żeby zanimować wielkość naszego wewnętrznego kwadratu należy zdefiniować nową animowalną wartość, od której uzależnimy jego wysokość i szerokość. Zamieńmy również komponent View na animowalny komponent Animated.View

const size = new Animated.Value(0);

<Animated.View
    style={[
        styles.insideBox,
        {
            height: size,
            width: size,
        },
    ]}
/>;

2. Teraz skonfigurujmy animację.

Najpierw wybierzmy jeden z trzech dostępnych typów animacji:

  • Animated.spring()
  • Animated.decay()
  • Animated.timing()

Ja wybrałem Animated.timing(), ale zachęcam do sprawdzenia jak działają inne typy. Wszystkie informacje dostępne są w dokumentacji.

Animated.timing() przyjmuje dwa argumenty:

  1. jednym jest wartość, która ma być animowana (w naszym przypadku size),
  2. drugim obiekt z wartościami konfigurującymi animację.

W obiekcie konfiguracyjnym zdefiniujmy 3 wartości:

  1. toValue odpowiadającą za to do jakiej wielkości ma zmniejszyć się nasze animowane View,
  2. duration odpowiadającą za czas trwania animacji w milisekundach oraz
  3. useNativeDriver (opcja odpowiadająca za poprawienie performance, która nie wspiera jednak animacji wartości height i width stąd ustawiamy ją na false).

Poniższy kod pokazuje dwie skonfigurowane animacje:

  1. jedna zmienia wartość size do 85 (odpowiada za animacje przy zaznaczaniu checkboxa),
  2. druga zmienia wartość size do 0 (odpowiada za animacje przy odznaczaniu checkboxa).
Animated.timing(size, {
    toValue: 85,
    duration: 350,
    useNativeDriver: false,
});
Animated.timing(size, {
    toValue: 0,
    duration: 350,
    useNativeDriver: false,
});

Teraz w hooku useEffect w zależności od wartości value wykonajmy .start(), co spowoduje rozpoczęcie odpowiedniej animacji.

useEffect(() => {
    if (value) {
        Animated.timing(size, {
            toValue: 85,
            duration: 350,
            useNativeDriver: false,
        }).start();
    } else {
        Animated.timing(size, {
            toValue: 0,
            duration: 350,
            useNativeDriver: false,
        }).start();
    }
}, [value]);

3. Gotowa animacja powinna wyglądać tak:

Zmiana wielkości wewnętrznego kwadratu | krok 2
Zmiana wielkości wewnętrznego kwadratu | krok 2

Zachęcam do zabawy z dostępnymi opcjami i konfiguracją animacji. Ja np. stworzyłem CheckBox z logo Dogtronic.

Checkbox z logo Dogtronic
Checkbox z logo Dogtronic

Animowana paginacja - animowanie zależne od scrollX

Animacje możemy również uzależnić od parametrów wejściowych.

W tym przykładzie pokażę w jaki sposób animować wielkość oraz kolor komponentu w zależności od tego w jakim stopniu została przescrollowana flat lista z obrazkami. 

Animacja wielkości oraz koloru komponentu
Animacja wielkości oraz koloru komponentu
Krok 1

Implementacja

Zacznijmy od zaimplementowania flat listy wyświetlającej obrazki wraz ze wskaźnikiem pokazującym który slajd jest aktualnie wyświetlany. 

1. Instalacja biblioteki swiper-flat-list.

Najpierw zainstalujmy bibliotekę swiper-flat-list zgodnie z instrukcjami.

import SwiperFlatList from "react-native-swiper-flatlist";

Utwórzmy teraz komponent flat-list. Otrzymuje on poprzez propsy tablicę obrazów. Szerokość obrazów uzależniona jest od szerokości ekranu poprzez zmienną windowWidth. Posiada on stan activeImage, który przechowuje informację o obecnie wyświetlanym obrazku.

2. Teraz zaimportujmy komponent swiper-flat-list.


interface Props {
    images: any[];
}

const windowWidth = Dimensions.get("window").width;

const PaginatedFlatlist: React.FC = ({ images }) => {
    const [activeImage, setActiveImage] = useState(0);

    return (
        <View>
            <SwiperFlatList onChangeIndex={({ index }) => setActiveImage(index)}>
                {images.map((image, index) => (
                    <Image
                        style={[styles.image, { width: windowWidth - 30 }]}
                        key={index}
                        source={image}
                        resizeMode="cover"
                    />
                ))}
            </SwiperFlatList>
        </View>
    );
};
      
      
export default PaginatedFlatlist;

Następnie dodajmy do komponentu paginacje. W tym celu użyjmy funkcji map na tablicy obrazów i wyświetlajmy małą czarną kropkę, lub jeśli indeks obrazu jest równy indeksowi obecnie wyświetlanego obrazu – większą niebieską kropkę.

3. Na tym etapie komponent powinien zachowywać się w następujący sposób:

Animacja wielkości oraz koloru komponentu | Krok 1
Animacja wielkości oraz koloru komponentu | Krok 1

import React, { useState } from "react";
import { Dimensions, Image, StyleSheet, View } from "react-native";
import SwiperFlatList from "react-native-swiper-flatlist";

interface Props {
    images: any[];
}

const windowWidth = Dimensions.get("window").width;

const PaginatedFlatlist: React.FC = ({ images }) => {
    const [activeImage, setActiveImage] = useState(0);

    return (
        <View>
            <SwiperFlatList onChangeIndex={({ index }) => setActiveImage(index)}>
                {images.map((image, index) => (
                    <Image
                        style={[styles.image, { width: windowWidth - 30 }]}
                        key={index}
                        source={image}
                        resizeMode="cover"
                    />
                ))}
            </SwiperFlatList>

            <View style={styles.paginationContainer}>
                {images.map((_, index) => {
                    return (
                        <View
                            style={[
                                styles.dot, 
                                activeImage === index && styles.activeDot
                            ]}
                            key={index}
                        />
                    );
                })}
            </View>
        </View>
    );
};


export default PaginatedFlatlist;

const styles = StyleSheet.create({
    image: {
        justifyContent: "center",
    },
    paginationContainer: {
        marginTop: 10,
        flexDirection: "row",
        justifyContent: "center",
        alignItems: "center",
    },
    dot: {
        height: 10,
        width: 10,
        margin: 7,
        borderRadius: 8,
        backgroundColor: "black",
    },
    activeDot: {
        height: 14,
        width: 14,
        backgroundColor: "#1a51b0",
    },
});
Krok 2

Animacja

W celu utworzenia animacji paginacji musimy w pierwszej kolejności uzyskać dostęp do wartości o jaką obecnie przescrollowany jest oponent flat list.

1. W tym celu należy zdefiniować zmienną scrollX.


const scrollX = useRef(new Animated.Value(0)).current;
 

Następnie w funkcji onScroll flat listy wykorzystamy Animated.event w celu zmapowania obecnego stopnia przescrollowania flaltlisty na zdefiniowaną wcześniej zmienną scrollX

onScroll={Animated.event(
    [{ nativeEvent: { contentOffset: { x: scrollX } } }],
    { useNativeDriver: false }
)}

2. Animację utworzymy korzystając z interpolacji.

Animajcę utworzymy korzystając z interpolacji. Polega to na tym, że tworzymy nową zmienną, której wartość zależna jest od obecnej wartości innej zmiennej typu Animated.Value.

 

W naszym przypadku utworzymy dwie zmienne:

  • dotColor odpowiadającą za zmianę koloru kropek,
  • oraz dotSize odpowiadającą za ich wielkość.

Będą one zależne od obecnej wartości scrollX.

Input range jest tablicą wartości scrollX przy jakich dotColor lub dotSize mają osiągać odpowiadające wartości z tablicy output range.

Wartości (index – 1) width,  (index) width i (index + 1) * width odpowiadają wartościom scrollX dla jakich powinna wyświetlać się duża niebieska kropka, kropka po niej następuje, oraz kropka je poprzedzająca.

const inputRange = [
        (index - 1) * width,
        index * width,
        (index + 1) * width,
];

const dotSize = scrollX.interpolate({
    inputRange,
    outputRange: [10, 14, 10],
    extrapolate: "clamp",
});

const dotColor = scrollX.interpolate({
    inputRange,
    outputRange: ["#000000", "#1a51b0", "#000000"],
    extrapolate: 'clamp'
});

Ustawienie wartości extrapolate na clamp sprawia, że biblioteka animated nie będzie próbowała obliczyć wartości output range, jeśli wartość scrollX wyjdzie poza zakres zdefiniowany w input range. Dzięki temu wielkość kropek paginacji nie będzie mniejsza od 10.

3. Ostatnim krokiem jest nadanie odpowiednich stylów kropkom paginacji.

<Animated.View
    style={[
        styles.dot,
        { width: dotSize, height: dotSize, backgroundColor: dotColor },
    ]}
    key={index}
/>

4. Ostatecznie animacja powinna wyglądać w następujący sposób:

Animacja wielkości oraz koloru komponentu
Animacja wielkości oraz koloru komponentu
Michał Matuła

Padawan React Native z aspiracjami na mistrza | Football, art & music

Zostaw komentarz

dwadzieścia + piętnaście =

Top