24 de março de 2020 às 12:30

Criando barra de progresso horizontal animada usando a Animated API no React-Native

Tempo de leitura: 8 minutos

Fala Galera Blz? estou aqui novamente para falar sobre animações no React-Native, esse é um assunto que me empolga bastante, por ser um dos pontos mais desafiadores e complexos quando trabalhamos com React-Native, Não pelo grau de dificuldade da Animated API ou do próprio React-Native, afinal ambos são de fácil compreensão, bastando algumas horas de leitura de suas respectivas documentações.

Um dos desafios aos quais me refiro é a carga de complexidade de animações envolvivas no projeto em si, passei por muitos projetos que utilizam React-Native e em sua maioria dificilmente repeti uma animação, num desses projetos me deparei com uma situação que a principio parecia simples, mas era só ilusão, pois no momento da implementação, mesmo conhecendo bem a Animated API, eu não fazia ideia de como implementar essa animação ou mesmo não fazia ideia de como uma animação tão simples funcionava.

A animação que me refiro é aquela barra horizontal de progresso, que geralmente é utilizada dentro de projetos que utilizam Material UI, sendo mais especifico falo da barra horizontal de progresso indeterminado, algo parecido com o que você vê na imagem abaixo.

https:::miro.medium.com:max:640:0*D8xdoQuah2-kahGX

Bom para não perder o costume, vou deixar aqui os pré-requisitos e conhecimentos desejáveis para você entender e implementar com sucesso esse tutorial.

Pré requisitos e conhecimentos prévios desejáveis:

Estou considerando neste post que você já sabe sobre layout e também criar um projeto em React-Native, dito isto, vou ignorar toda a parte de criação e configuração do projeto.

  • React Native
  • Layout com FlexBox
  • react-native-cli

    Criando o container para nossa animação

Eu costumo ser mais restrito quando se trata de utilizar bibliotecas (libs) que fazem algo que eu conseguiria fazer em até 160 linhas de código, este apenas é um número que acredito ser o ideal para a leitura do código não ficar massiva, então vou me limitar neste post a fazer algo em até 160 linhas, utilizando apenas o essencial.

A primeira coisa que vou fazer é criar um layout básico para comportar minha animação, vou dividir a tela em duas seções, onde na seção do topo vou dizer ao usuário o que está acontecendo, e na seção inferior poderia colocar algum outro tipo de informação como um call to action por exemplo.

/**
 * Horizontal Progress Bar com Animated API
 * https://github.com/facebook/react-native
 *
 * @format
 * @flow
 */
import React from 'react';
import {Animated, SafeAreaView, StyleSheet, Text, View} from 'react-native';

const App: () => React$Node = () => {
  return (
    <SafeAreaView>
      <View style={styles.headerContentContainer}>
        <Text style={styles.title1}>
          O Aplicativo está sincronizando seus dados.
        </Text>
      </View>
      <Animated.View style={styles.syncProgressBarContainer}>
        <Animated.View style={styles.syncProgressBar} />
        <Animated.View style={styles.syncProgressBar} />
        <Animated.View style={styles.syncProgressBar} />
        <Animated.View style={styles.syncProgressBar} />
      </Animated.View>
      <View style={styles.syncContentContainer}>
        <Text style={styles.title3}>Não feche ou saia do aplicativo.</Text>
        <Text style={styles.body2}>
          Você pode aproveitar para fazer um alongamento.
        </Text>
      </View>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  headerContentContainer: {
    paddingHorizontal: 25,
    paddingTop: 40,
    paddingBottom: 20,
  },
  syncContentContainer: {
    paddingHorizontal: 25,
    paddingTop: 40,
    paddingBottom: 20,
  },
  title1: {
    fontWeight: '700',
    fontSize: 32,
  },
  title3: {
    fontWeight: '700',
    fontSize: 18,
  },
  body2: {
    fontSize: 14,
  },
  syncProgressBarContainer: {
    flexDirection: 'row',
  },
  syncProgressBar: {
    height: 4,
    marginHorizontal: 10,
    width: 200,
    backgroundColor: '#0000ff',
  },
});

export default App;

No código acima basicamente declarei Views e Animated.Views e apliquei alguns estilos nelas, nada demais até então, é importante ressaltar que qualquer Elemento que será animado deve vir da Animated API, repare que nos casos dos elementos que representarão as barras crio uma instância da Animated.View.

Criando as partes da animação

Para criarmos nossa animação vamos precisar trabalhar com o atributo translate, mais especificamente o translateX já que estamos lidando com animação horizontal.

O primeiro passo é criar meu Animated.Value o qual sofrerá as mutações e irá traduzi-las para a UI.

const [offsetX] = useState(new Animated.Value(-400));

Repare que estou inicializando o valor com -400, estou fazendo isso pois quero que minhas barras começem a animação escondidas à esquerda na tela e na sequência passem a ir para a direita.

const translate = Animated.timing(offsetX, {
  toValue: 0,
  duration: 1000,
  easing: Easing.inOut(Easing.linear),
  useNativeDriver: true,
});

Em seguida criei o efeito de ínicio da minha animação, basicamente estou dizendo que para aquele valor declarado anteriormente deve se tornar 0 em 1 segundo, ou seja deve sair da posição -400 e ir para a posição 0 da tela.

Agora preciso de alguma forma encaixar isso num loop, para isso vou precisar de uma animação de reset.

const reset = Animated.timing(offsetX, {
  toValue: -430,
  duration: 0,
  useNativeDriver: true,
});

Neste timing eu estou colocando um valor um pouco inferior a -400, apenas para que minha animação fique mais fluida devido à duração que agora é 0, o que causaria um impacto na minha animação caso a retornasse as barras para posição -400.

Tudo que fizemos até aqui foi criar o layout e declarar as duas etapas da nossa animação e é agora que efetivamente vamos declarar a animação propriamente dita, animação que será uma sequencia de passos onde as barras sofrerão translate em direção à direita em seguida voltarão um pouco antes da posição inicial de forma instantânea.

Criando a sequência de animações

const animation = Animated.sequence([translate, reset]);

Simplesmente só preciso dizer através da Animated API que quero que essa animação seja uma sequência de outras animações e passar um array com a ordem que quero que as animações aconteçam. Inicializando a animação

Agora só falta iniciar a animação dentro do componente, vou fazer isso utilizando o hook useEffect.

useEffect(() => {
  Animated.loop(animation).start();

  // Substitua esse setTimeout por uma chamada http ou qualquer outra chamada de serviço.
  setTimeout(() => {
    console.log('timeout')
  }, 4000);
}, [animation, navigate]);

O objetivo deste post não é tratar sobre hooks, mas vou deixar um link para o artigo do hook useEffect caso tenha alguma dúvida de como ele funciona, dentro do handler do hook basicamente digo que quero que a Animated API faça um loop() de uma animação em seguida executo a animação com start().

Show de bola a animação esta definida, mas algo de errado não está certo, ainda falta algo.

Interpolando o Animated.Value para os estilos

Lembra de quando eu disse que era importante usarmos Animated.View em vez de View? pois é, vamos usar o nosso Animated.Value o qual esta sendo constantemente transformado como valor do atributo translate de cada Animated.View que utilizamos para as barras.

const transform = {transform: [{translateX: offsetX}]};

E vamos aplicar esse valor para os estilos da nossa barra.

<Animated.View style={[transform, styles.syncProgressBar]} />

E este é o arquivo final, como prometido em menos de 160 linhas, 98 pra ser mais especifico.

/**
 * Horizontal Progress Bar com Animated API
 * https://github.com/facebook/react-native
 *
 * @format
 * @flow
 */
import React, {useEffect, useState} from 'react';
import {
  Animated,
  Easing,
  SafeAreaView,
  StyleSheet,
  Text,
  View,
} from 'react-native';

const App: () => React$Node = () => {
  const [offsetX] = useState(new Animated.Value(-400));
  const translate = Animated.timing(offsetX, {
    toValue: 0,
    duration: 1000,
    easing: Easing.inOut(Easing.linear),
    useNativeDriver: true,
  });
  const reset = Animated.timing(offsetX, {
    toValue: -430,
    duration: 0,
    useNativeDriver: true,
  });
  const animation = Animated.sequence([translate, reset]);
  useEffect(() => {
    Animated.loop(animation).start();

    // Substitua esse setTimeout por uma chamada http ou qualquer outra chamada de serviço.
    setTimeout(() => {
      console.log('Chamar serviço');
    }, 4000);
  }, [animation]);
  const transform = {transform: [{translateX: offsetX}]};
  return (
    <SafeAreaView>
      <View style={styles.headerContentContainer}>
        <Text style={styles.title1}>
          O Aplicativo está sincronizando seus dados.
        </Text>
      </View>
      <Animated.View style={styles.syncProgressBarContainer}>
        <Animated.View style={[transform, styles.syncProgressBar]} />
        <Animated.View style={[transform, styles.syncProgressBar]} />
        <Animated.View style={[transform, styles.syncProgressBar]} />
        <Animated.View style={[transform, styles.syncProgressBar]} />
      </Animated.View>
      <View style={styles.syncContentContainer}>
        <Text style={styles.title3}>Não feche ou saia do aplicativo.</Text>
        <Text style={styles.body2}>
          Você pode aproveitar para fazer um alongamento.
        </Text>
      </View>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  headerContentContainer: {
    paddingHorizontal: 25,
    paddingTop: 80,
    paddingBottom: 40,
  },
  syncContentContainer: {
    paddingHorizontal: 25,
    paddingTop: 40,
    paddingBottom: 20,
  },
  title1: {
    fontWeight: '700',
    fontSize: 32,
  },
  title3: {
    fontWeight: '700',
    fontSize: 18,
  },
  body2: {
    fontSize: 14,
  },
  syncProgressBarContainer: {
    flexDirection: 'row',
  },
  syncProgressBar: {
    height: 4,
    marginHorizontal: 10,
    width: 200,
    backgroundColor: '#0000ff',
  },
});

export default App;

Este é o resultado final: https:::miro.medium.com:max:640:0*D8xdoQuah2-kahGX

Este é o link do projeto no github caso queira modificar a partir dele: https://github.com/digital-heroes/horizontal-progress-bar.git

I-Isso, é tudo pe-pe-pe-pe-pe-pessoal, espero que tenha ajudado, qualquer dúvida fiquem a vontade para comentar, não se esqueçam de apertar algumas vezes no botão de aplausos, isso é muito importante para mim pois me incentiva a continuar escrevendo e compartilhando experiências. Aguardo vocês no próximo post! Até lá!