Animações do After Effects no React Native com Lottie

React Native 17 de Jun de 2020

Aplicativos com animações ficam bem bonitos, legais e dá vontade de mexer.

Neste post apresentaremos o Lottie, como instalar no Expo e colocar uma animação no projeto Ecoleta da primeira NLW (trilha booster), e instalar no projeto de autenticação feito com React Native CLI, por fim mostrar os resultados na tela.📱

After Effects

Podemos construir as nossas animações interativas do zero com After Effects da Adobe e exportar a animação em formato JSON e colocar na pasta assets do projeto web ou mobile de forma rápida e fácil. Depois da animação pronta você pode customizar as cores ou a duração da animação. 👌

Lottie Files
O Airbnb  desenvolveu uma ferramenta Lottie, que agora está disponível no React Native Community: O site reúne várias animações que você pode baixar e utilizar no projeto de forma gratuita e customiza-las.

React Native na prática - Evento online | Rocketseat
Crie dois projetos mobile para Android e iOS com uma única base de código em duas aulas 100% práticas.

Navegando no site e customizando uma animação:
Veja no vídeo de um minuto como é fácil escolher uma animação, customizar e baixar.

Basicamente você busca uma animação, faz o download do arquivo json e coloca na pasta assets do projeto e importa no componente Lottie.

API básica do Lottie

As propriedades mais utilizadas são:
👉 source: onde você adiciona o caminho para o arquivo json da animação usando require('../path/to/animation.json');
👉  loop: serve para deixar a animação sempre executando;
👉 autoPlay: serve para iniciar a animação assim que aparecer na tela.

Para mais detalhes veja na doc, mas só com essas propriedades já dá para fazer muita coisa legal.

Instalando o Lottie Rect Native no Expo

Se você já criou um projeto no Expo é muito fácil adicionar o Lottie nele, seguindo a documentação você precisa apenas adicionar a biblioteca no projeto:

expo install lottie-react-native

Fechar o projeto e depois reiniciar:

expo start

Pronto, já vai estar instalado e pronto para ser utilizado. 🏎️

Integrando o Lottie no Projeto Ecoleta

Se você fez a primeira Next Level Week na trilha booster você já tem o projeto mobile criado com Expo. Link do repositório.

Eu escolhi um loading map para mostrar na tela enquanto o mapa não é renderizado.

OBSERVAÇÃO 👀

Eu tinha escolhido esse arquivo, mas ele deu problema no meu celular Android, o app fechava do nada e vi que tinha algum problema de incompatibilidade de versão do arquivo, eu não quis perder muito tempo com isso então escolhi esse gif acima que você esta vendo. Então se der problema para você, provavelmente não é seu projeto que está com problema e sim a versão do Lottie File (json) que você está usando.

Eu baixei o arquivo e renomeei para loading.json e coloquei na pasta src/assets do projeto.

Para instalar o Lottie no projeto mobile da NLW eu fiz os passos anteriores de instalação da biblioteca e reiniciei o projeto, e adicionei no código:

Primeiro eu importei no arquivo src/pages/Points/index.tsx:

import LottieView from  "lottie-react-native";

Criei um novo estado (loading) com useState e inicializei como true.

const [loading, setLoading] =  useState(true);

Porque sempre que o componente Points é montado em tela o useEffect que busca a posição no mapa vai ser chamado e isso demora alguns segundos. Nesse meio tempo coloco a animação para executar e paro ela quando o mapa está pronto, alterando o estado de loading para false.

React Native na prática - Evento online | Rocketseat
Crie dois projetos mobile para Android e iOS com uma única base de código em duas aulas 100% práticas.

No final do processo de loadPosition eu defino o loading como false, com setLoading(false);

useEffect(() => {
  async function loadPosition() {
    const { status } = await Location.requestPermissionsAsync();

    if (status !== "granted") {
      Alert.alert(
        "Ops!", "Precisamos de sua permissão para obeter a localização"
      );

      return;
    }

    const location = await Location.getCurrentPositionAsync();

    const { latitude, longitude } = location.coords;

    setInitialPosition([latitude, longitude]);

    setLoading(false);
  }

  loadPosition();
}, []);

Utilizo o LottieView no componente:

<LottieView
  source={require("../../assets/loading.json")}
  loop
  autoPlay
/>

Veja como fica:

<View style={styles.mapContainer}>
  {loading ? (
    <LottieView source={require("../../assets/loading.json")} loop autoPlay />
  ) : (
    <MapView
      style={styles.map}
      loadingEnabled={initialPosition[0] === 0}
      initialRegion={{
        latitude: initialPosition[0],
        longitude: initialPosition[1],
        latitudeDelta: 0.014,
        longitudeDelta: 0.014,
      }}
    >
      {points.map((point) => (
        <Marker
          key={point.id}
          style={styles.mapMarker}
          onPress={() => handleNavigateToDetail(point.id)}
          coordinate={{
            latitude: point.latitude,
            longitude: point.longitude,
          }}
        >
          <View style={styles.mapMarkerContainer}>
            <Image
              style={styles.mapMarkerImage}
              source={{
                uri: point.image_url,
              }}
            />
            <Text style={styles.mapMarkerTitle}>{point.name}</Text>
          </View>
        </Marker>
      ))}
    </MapView>
  )}
</View>;

Faço uma condição que verifica se o loading estiver true então renderiza o LottieView senão renderiza o MapView.

Resultado em tela:

Poderíamos fazer diferente também, colocar um if antes do return principal e fazer o loading na tela toda, que fica bem legal também:

  if (loading) {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <View style={styles.container}>
        <LottieView
          source={require("../../assets/loading.json")}
          loop
          autoPlay
        />
      </View>
    </SafeAreaView>
  );
}

Resultado:

Veja o código do componente todo para ficar melhor para entender o fluxo:

import React, { useState, useEffect } from "react";
import {
  View,
  Text,
  TouchableOpacity,
  StyleSheet,
  ScrollView,
  Image,
  SafeAreaView,
  Alert,
} from "react-native";
import { useNavigation, useRoute } from "@react-navigation/native";
import { Feather as Icon } from "@expo/vector-icons";
import MapView, { Marker } from "react-native-maps";
import { SvgUri } from "react-native-svg";
import Constants from "expo-constants";
import api from "../../services/api";
import * as Location from "expo-location";
import LottieView from "lottie-react-native";

interface Item {
  id: number;
  title: string;
  image_url: string;
}

interface Point {
  id: number;
  name: string;
  image: string;
  latitude: number;
  longitude: number;
  image_url: string;
}

interface Params {
  uf: string;
  city: string;
}

const Points = () => {
  const navigation = useNavigation();
  const route = useRoute();

  const routeParams = route.params as Params;
  const [items, setItems] = useState<Item[]>([]);
  const [initialPosition, setInitialPosition] = useState<[number, number]>([0, 0]);
  const [selectedItems, setSelectedItems] = useState<number[]>([]);
  const [points, setPoints] = useState<Point[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    api.get("/items").then((response) => {
      setItems(response.data);
    });
  }, []);

  useEffect(() => {
    async function loadPosition() {
      const { status } = await Location.requestPermissionsAsync();

      if (status !== "granted") {
        Alert.alert(
          "Ops!",
          "Precisamos de sua permissão para obeter a localização"
        );
        return;
      }

      const location = await Location.getCurrentPositionAsync();

      const { latitude, longitude } = location.coords;

      setInitialPosition([latitude, longitude]);
      setLoading(false);
    }

    loadPosition();
  }, []);

  useEffect(() => {
    api
      .get("/points", {
        params: {
          city: routeParams.city,
          uf: routeParams.uf,
          items: selectedItems,
        },
      })
      .then((response) => {
        setPoints(response.data);
      });
  }, [selectedItems]);

  function handleNavigateToDetail(id: number) {
    navigation.navigate("Detail", { point_id: id });
  }

  function handleSelectItem(id: number) {
    const alreadySelected = selectedItems.includes(id);
    if (alreadySelected) {
      setSelectedItems([
        ...selectedItems.filter((idFiltered) => idFiltered !== id),
      ]);
    } else {
      setSelectedItems([...selectedItems, id]);
    }
  }

  // if (loading) {
  //   return (
  //     <SafeAreaView style={{ flex: 1 }}>
  //       <View style={styles.container}>
  //         <LottieView
  //           source={require("../../assets/loading.json")}
  //           loop
  //           autoPlay
  //         />
  //       </View>
  //     </SafeAreaView>
  //   );
  // }

  return (
    <SafeAreaView style={{ flex: 1 }}>
      <View style={styles.container}>
        <TouchableOpacity onPress={() => navigation.goBack()}>
          <Icon name="arrow-left" color="#34cb79" size={20} />
        </TouchableOpacity>

        <Text style={styles.title}>♻️ Bem vindo.</Text>
        <Text style={styles.description}>
          Encontre no mapa um ponto de coleta.
        </Text>

        <View style={styles.mapContainer}>
          {loading ? (
            <LottieView
              source={require("../../assets/loading.json")}
              loop
              autoPlay
            />
          ) : (
            <MapView
              style={styles.map}
              loadingEnabled={initialPosition[0] === 0}
              initialRegion={{
                latitude: initialPosition[0],
                longitude: initialPosition[1],
                latitudeDelta: 0.014,
                longitudeDelta: 0.014,
              }}
            >
              {points.map((point) => (
                <Marker
                  key={point.id}
                  style={styles.mapMarker}
                  onPress={() => handleNavigateToDetail(point.id)}
                  coordinate={{
                    latitude: point.latitude,
                    longitude: point.longitude,
                  }}
                >
                  <View style={styles.mapMarkerContainer}>
                    <Image
                      style={styles.mapMarkerImage}
                      source={{
                        uri: point.image_url,
                      }}
                    />
                    <Text style={styles.mapMarkerTitle}>{point.name}</Text>
                  </View>
                </Marker>
              ))}
            </MapView>
          )}
        </View>
      </View>
      <View style={styles.itemsContainer}>
        <ScrollView
          horizontal
          showsHorizontalScrollIndicator={false}
          contentContainerStyle={{ paddingHorizontal: 20 }}
        >
          {items.map((item) => (
            <TouchableOpacity
              key={String(item.id)}
              style={[
                styles.item,
                selectedItems.includes(item.id) ? styles.selectedItem : {},
              ]}
              activeOpacity={0.6}
              onPress={() => handleSelectItem(item.id)}
            >
              <SvgUri width={42} height={42} uri={item.image_url} />
              <Text style={styles.itemTitle}>{item.title}</Text>
            </TouchableOpacity>
          ))}
        </ScrollView>
      </View>
    </SafeAreaView>
  );
};

export default Points;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingHorizontal: 32,
    paddingTop: 20 + Constants.statusBarHeight,
  },

  title: {
    fontSize: 20,
    fontFamily: "Ubuntu_700Bold",
    marginTop: 24,
  },

  description: {
    color: "#6C6C80",
    fontSize: 16,
    marginTop: 4,
    fontFamily: "Roboto_400Regular",
  },

  mapContainer: {
    flex: 1,
    width: "100%",
    borderRadius: 10,
    overflow: "hidden",
    marginTop: 16,
  },

  map: {
    width: "100%",
    height: "100%",
  },

  mapMarker: {
    width: 90,
    height: 80,
  },

  mapMarkerContainer: {
    width: 90,
    height: 70,
    backgroundColor: "#34CB79",
    flexDirection: "column",
    borderRadius: 8,
    overflow: "hidden",
    alignItems: "center",
  },

  mapMarkerImage: {
    width: 90,
    height: 45,
    resizeMode: "cover",
  },

  mapMarkerTitle: {
    flex: 1,
    fontFamily: "Roboto_400Regular",
    color: "#FFF",
    fontSize: 13,
    lineHeight: 23,
  },

  itemsContainer: {
    flexDirection: "row",
    marginTop: 16,
    marginBottom: 32,
  },

  item: {
    backgroundColor: "#fff",
    borderWidth: 2,
    borderColor: "#eee",
    height: 120,
    width: 120,
    borderRadius: 8,
    paddingHorizontal: 16,
    paddingTop: 20,
    paddingBottom: 16,
    marginRight: 8,
    alignItems: "center",
    justifyContent: "space-between",

    textAlign: "center",
  },

  selectedItem: {
    borderColor: "#34CB79",
    borderWidth: 2,
  },

  itemTitle: {
    fontFamily: "Roboto_400Regular",
    textAlign: "center",
    fontSize: 13,
  },
});

Ficou bem bonito né, já dá outra cara para o App. 🎉

Como instalar no React Native CLI:

Para ganhar tempo, vou aproveitar o projeto do tutorial que fizemos, você pode baixar o código aqui e seguir os passos.

Instalando o Lottie.


Na raiz do projeto executo o comando:

npm i --save lottie-react-native lottie-ios@3.1.3

Se estiver executando o projeto no simulador do iOS então precisa executar um pod install na pasta ios:

cd ios && pod install

Como executei um pod install preciso fazer o build do projeto novamente:

npx react-native run-ios

No Android o auto linking já funciona, mas qualquer problema que vier a surgir você pode investigar aqui.

Pronto, projeto funcionando, o Lottie já está pronto para ser utilizado.

Criei um pasta src/assets/lotties no projeto coloquei os dois arquivos loading.json e security.json (renomeei os dois arquivos, você pode fazer isso sem problemas) que são as duas animações que baixei no Lottie Files.

Criei dois componentes na pasta src/components que encapsulam o funcionamento do Loading e a sua estilização:

src/componentes/Loading.tsx que será utilizando na tela quando abre o app.

import React from "react";
import LottieView from "lottie-react-native";
import { View } from "react-native";

const Loading: React.FC = () => {
  return (
    <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
      <LottieView
        source={require("../assets/lotties/loading.json")}
        autoPlay
        speed={2.5}
        loop
      />
    </View>
  );
};

export default Loading;

src/componentes/LoadingSignIn.tsx  será utilizando quando clicar no botão de SignIn e simular o loading do processo de autenticação na aplicação.

import React from "react";
import LottieView from "lottie-react-native";
import { View } from "react-native";

const LoadingSignIn: React.FC = () => {
  return (
    <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
      <LottieView
        source={require("../assets/lotties/security.json")}
        autoPlay
        loop
      />
    </View>
  );
};

export default LoadingSignIn;

No arquivo src/routes/index.tsx eu utilizo o primeiro componente Loading:

import React from "react";
import { useAuth } from "../contexts/auth";
import AuthRoutes from "../routes/auth.routes";
import AppRoutes from "../routes/app.routes";

import Loading from "../components/Loading";

const Routes: React.FC = () => {
  const { signed, loading } = useAuth();

  if (loading) {
    return <Loading />;
  }

  return signed ? <AppRoutes /> : <AuthRoutes />;
};

export default Routes;

Quando a aplicação inicia o componente Routes é executado e temos um loading da aplicação porque é feito um processo para verificar se a autenticação do usuário continua válida, e a rota para qual iremos enviar o usuário depende desse processo e para o usuário não ver uma tela em branco, usamos o componente de animação executando.

Resultado:

E outro cenário é quando usuário clica no botão SignIn para se autenticar, temos um loading também para aguardar o processo de validação do login, temos uma outra animação.

React Native na prática - Evento online | Rocketseat
Crie dois projetos mobile para Android e iOS com uma única base de código em duas aulas 100% práticas.

src/pages/SignIn/index.tsx:

import React, { useState } from "react";
import { View, Button, StyleSheet } from "react-native";
import { useAuth } from "../../contexts/auth";

import LoadingSignIn from "../../components/LoadingSignIn";

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
  },
});

const SignIn: React.FC = () => {
  const { signIn } = useAuth();
  const [loading, setLoading] = useState(false);

  async function handleSign() {
    setLoading(true);
    await signIn();
    setLoading(false);
  }

  if (loading) {
    return (
      <View style={styles.container}>
        <LoadingSignIn />
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Button title="Sign In" onPress={handleSign} />
    </View>
  );
};

export default SignIn;

Resultado:

Ficou bem legal né, algo simples que vai deixando a aplicação bem mais bonita e legal para utilizar, levando a aplicação para um próximo nível.

Se você curtiu esse conteúdo, deixa seu like e comente se foi relevante para você. 💜

Temos um vídeo no youtube onde o Diego Fernandes apresenta essa ferramenta também:

Até a próxima! 😎

O aprendizado é contínuo e sempre haverá um próximo nível!

Marcadores

Thiago Marinho

Dev Apaixonado por Tecnologia & Educação! Evolua rápido como a tecnologia, aprendizado é contínuo e sempre haverá um próximo nível. Boost Yourself! 🚀