react-native-reanimated: Um guia prático

Kauê da Maia
8 min readOct 17, 2019

--

Essa é sua thread Javascript sem react-native-reanimated. WonderlaneUnsplash

Animações são essenciais para entregar uma experiência fluída para o usuário. O uso do movimento entre dois ou mais estados facilita o uso da aplicação, reduzindo a carga cognitiva e inferindo funcionalidades que podem parecer óbvias para o desenvolvedor mas nem tão óbvias assim para o consumidor.

Se você já usou a Animated API do React Native, deve ter reparado que quanto mais coisas seu app está fazendo, mais "travadas" são as animações. Isso se dá pelo fato de que quando uma animação é executada, são necessárias diversas chamadas entre o lado nativo e o lado Javascript do aplicativo, ou seja, se o Javascript estiver executando uma tarefa muito custosa, as animações vão ter que esperar a conclusão desta tarefa para só então serem executadas.

Para evitar esse e outros problemas, podemos utilizar a biblioteca react-native-reanimated. Para entender como a Animated API funciona e como o reanimated resolve diversas falhas, veja a talk do Krzysztof, autor da biblioteca, na React Europe:

Mãos à obra

Usando o reanimated vamos fazer algo bem simples: uma lista horizontal de cards, onde os itens que não estão ativos ficarão menores e com uma opacidade reduzida.

Começamos definindo a largura que os nossos cards terão. No nosso exemplo, quero que cada um deles ocupe a largura total da tela, deixando um espaço de 50 pixels de cada lado:

Após isso, vamos montar a nossa lista. Atente-se que são utilizados os componentes View e ScrollView do reanimated:

Tá achando que é muita coisa de uma vez só? Sem problemas, tá aqui a explicação:

  • snapToAlignment: local onde a ScrollView vai alinhar os itens. Foi escolhido right pra manter os itens sempre à direita.
  • decelerationRate: taxa de desaceleração da rolagem, ou seja, a velocidade com que a lista vai perder o impulso quando o usuário tirar o dedo da tela. Se você não está fazendo o aplicativo do pião da casa própria, não há motivo pra não usarfast .
Você tá ouvindo a música do pião na sua cabeça agora, né?
  • snapToInterval: Essa propriedade define a medida de cada item (no nosso caso, largura) e alinha na posição definida em snapToAlignment . Pra entender, nada melhor do que testar: Tente mover a lista de um lado pro outro. Reparou que não tem como parar o scroll no meio de dois items? Pois é, isso é snapToAlignment + snapToInterval funcionando.
  • contentContainerStyle: Usando snapToAlignment='right' os itens são jogados pra direita. Pra centralizar, aplicamos paddingHorizontal no conteúdo. paddingHorizontal é a mesma coisa que informar paddingLeft e paddingRight .

Por fim, geramos os nossos cards usando Array.from() . Essa borda vermelha aí é pra diferenciar um card do outro, já que ainda não tem espaçamento entre eles.

Agora que temos a nossa estrutura construída, vamos adicionar um pouco de mágica. O primeiro passo é capturar a posição atual do scroll da nossa lista e armazenar em algum lugar. Para isso vamos criar um novo reanimated value chamado offsetX :

O segundo passo é adicionar o evento que vai monitorar o gesto do usuário, conectando ele ao método onScroll da ScrollView . Pra entender melhor como esses eventos funcionam, você pode conferir a documentação oficial, lembrando que as duas opções mostradas, listener e useNativeDriver não existem no reanimated, apenas na Animated API.

Além disso, vamos inserir mais uma propriedade, scrollEventThrottle . Essa propriedade vai ser responsável por controlar quantas vezes o evento onScroll vai ser disparado, levando em conta que quanto mais baixo for o valor, mais vezes o evento é disparado e consequentemente o valor de offsetX é mais preciso. A documentação do React Native não recomenda usar um scrollEventThrottle muito baixo para evitar problemas de performance, mas como estamos usando o reanimated, podemos abusar dessa propriedade:

Agora que sabemos qual é a posição do scroll da lista, podemos construir interpolações baseadas nesse valor para animar os nossos itens. Não sabe o que é interpolação? Vamos recorrer a Wikipedia:

Interpolação é o método de aproximar os valores dos conjuntos discretos. Em [sic] matemática, denomina-se interpolação o método que permite construir um novo conjunto de dados a partir de um conjunto discreto de dados pontuais previamente conhecidos.

Pra aplicar isso com o reanimated, vamos utilizar a função interpolate . Essa função recebe como parâmetro pelo menos dois conjuntos de dados, chamados de inputRange e outputRange . Imagine o seguinte cenário:

Baseados nestes conjuntos a função de interpolação, que por padrão é linear (ela aceita funções de easing , mas vamos manter as coisas simples), vai mapear os valores entre os dois:

Como é possível reparar, se o nosso input for maior do que o máximo estabelecido em inputRange , o interpolate vai calcular mesmo assim. Caso isso não seja ideal para seu cenário, pode utilizar o parâmetro extrapolate para limitar o valor de saída. Se o objetivo for limitar apenas para o que foi definido em outputRange , Extrapolate.CLAMP resolve o problema. Você pode conferir as opções disponíveis na documentação.

Vale lembrar que ambos os conjuntos devem possuir pelo menos dois valores e inputRange deve ser sempre ascendente, ou seja, do menor pro maior.

Dadas as devidas explicações, vamos voltar para a nossa lista. Como foi dito lá no começo, a ideia é diminuir o tamanho dos itens que não estão ativos. Para isso, vamos interpolar o valor de scale dos nossos itens:

O inputRange está usando as funções do reanimated para gerar os valores de forma dinâmica baseado no index, definindo os valores para o item anterior, o item atual e o próximo item, ou seja, o item no centro da tela mantém o tamanho original (o 1 no inputRange ) enquanto os itens da esquerda e da direita são reduzidos em 90% (os dois 0.9 ). Simples, não?

Para concluir a lista, vamos aplicar a interpolação da opacidade para aumentar o destaque do item ativo:

Com essas duas interpolações simples, esse é o nosso resultado:

Nesse ponto você deve ter se perguntado: "tá, mas e se eu quiser iniciar uma animação a partir de uma função?". Para responder essa questão, vamos adicionar um botão para fazer nossa lista aparecer e desaparecer. Começamos modificando a nossa estrutura, envolvendo todo o conteúdo com um <React.Fragment> e adicionando o código do botão:

As animações escritas com o reanimated são bem extensas e você pode entender o motivo e o funcionamento delas na documentação. Para agilizar o nosso trabalho, vamos instalar uma biblioteca de utilidades chamada react-native-redash . O que essa biblioteca faz é entregar diversas abstrações construídas com o reanimated, inclusive os métodos de animação. Além disso, vamos também desestruturar mais alguns métodos de Animated que iremos utilizar:

DISCLAIMER: useCode é um hook que só vai funcionar a partir do React Native 0.59. Para versões anteriores, utilize <Animated.Code> .

Para fazer a mudança de opacidade precisamos de dois novos Values , um para armazenar o valor da opacidade e outro para controlar o estado da animação. Vamos chamá-las de carouselOpacity e animationState:

Uma das funções que desestruturamos de Animated é cond , que não é nada mais do que um if — else . A estrutura dos parâmetros é a seguinte:

Caso o valor de condição seja 1 , o retorno de cond será o que chamei no exemplo acima de callbackTrue , caso contrário, callbackFalse . Ambos os callbacks podem ser uma função do reanimated ou um número. callbackFalse é opcional e caso não seja informado, possui valor padrão 0 .

Outra função que extraímos é eq :

eq é um comparador de valores. O valor da esquerda é comparado com o da direita e se forem iguais, retorna 1 . Se forem diferentes, retorna 0 .

Tendo isso em mente, vamos adicionar nossa função que altera o valor de animationState . Todo Value de Animated possui um método chamado setValue , que é o que iremos utilizar:

Lendo a função de dentro pra fora, fica claro o que está acontecendo:

  • eq valida se animationState é igual a 0
  • cond compara o resultado de eq com 1 . Se eq = 0 , o valor de animationState é alterado para 1
  • como omitimos o callbackFalse de cond , caso a validação acima seja 0 , o valor de animationState é alterado para 0

Agora temos um botão que altera o valor de animationState quando é clicado, mas cadê a animação?

Pois bem, é aí que entra o hook useCode :

Confuso? Sem problemas. Vamos quebrar em partes:

  • timing é uma função que importamos do react-native-redash . Ela recebe alguns parâmetros, entre eles from , to e duration . from é o valor inicial da animação, no nosso caso é o valor atual de carouselOpacity . to é o valor de animationState , que nós controlamos na função toggle . duration define o tempo que a animação vai durar.
  • set recebe dois parâmetros: o Value que queremos alterar (nesse caso, carouselOpacity ) e o novo valor, que vai ser definido por timing .
  • por fim, adicionamos animationState dentro do array de dependências de useCode , fazendo com que esse hook seja executado toda vez que o valor de animationState for alterado.

Depois de tudo isso, temos o seguinte resultado:

Você encontra o código no repositório do GitHub

Olá, sou o Kauê. Espero que você tenha aprendido algo novo com o que eu escrevi. Você pode me encontrar no GitHub, LinkedIn ou Twitter. Eu também passo um bom tempo ajudando a comunidade de React Native no Telegram. Caso tenha alguma dúvida, entra lá :)

--

--