react-native-reanimated: Um guia prático
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 aScrollView
vai alinhar os itens. Foi escolhidoright
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
.
snapToInterval:
Essa propriedade define a medida de cada item (no nosso caso, largura) e alinha na posição definida emsnapToAlignment
. 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:
UsandosnapToAlignment='right'
os itens são jogados pra direita. Pra centralizar, aplicamospaddingHorizontal
no conteúdo.paddingHorizontal
é a mesma coisa que informarpaddingLeft
epaddingRight
.
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 seanimationState
é igual a0
cond
compara o resultado deeq
com1
. Seeq = 0
, o valor deanimationState
é alterado para1
- como omitimos o
callbackFalse
decond
, caso a validação acima seja0
, o valor deanimationState
é alterado para0
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 doreact-native-redash
. Ela recebe alguns parâmetros, entre elesfrom
,to
eduration
.from
é o valor inicial da animação, no nosso caso é o valor atual decarouselOpacity
.to
é o valor deanimationState
, que nós controlamos na funçãotoggle
.duration
define o tempo que a animação vai durar.set
recebe dois parâmetros: oValue
que queremos alterar (nesse caso,carouselOpacity
) e o novo valor, que vai ser definido portiming
.- por fim, adicionamos
animationState
dentro do array de dependências deuseCode
, fazendo com que esse hook seja executado toda vez que o valor deanimationState
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á :)