Animações
As animações são muito importantes para criar uma ótima experiência do usuário. Objetos estacionários devem superar a inércia à medida que começam a se mover. Objetos em movimento têm impulso e raramente param imediatamente. As animações permitem transmitir movimentos fisicamente verossímeis em sua interface.
React Native fornece dois sistemas de animação complementares: Animated
para controle granular e interativo de valores específicos e LayoutAnimation
para transações de layout global animadas.
Animated
API
A API Animated
foi projetada para expressar de forma concisa uma ampla variedade de padrões interessantes de animação e interação com muito desempenho. Animated
concentra-se em relacionamentos declarativos entre entradas e saídas, com transformações configuráveis entre elas e métodos de start
/stop
para controlar a execução da animação baseada em tempo.
Animated
exporta seis tipos de componentes animáveis: View
, Text
, Image
, ScrollView
, FlatList
e SectionList
, mas você também pode criar o seu próprio usando Animated.createAnimatedComponent()
.
Por exemplo, uma visualização de contêiner que aparece gradualmente quando é montada pode ter esta aparência:
import React, {useRef, useEffect} from 'react';
import {Animated, Text, View} from 'react-native';
import type {PropsWithChildren} from 'react';
import type {ViewStyle} from 'react-native';
type FadeInViewProps = PropsWithChildren<{style: ViewStyle}>;
const FadeInView: React.FC<FadeInViewProps> = props => {
const fadeAnim = useRef(new Animated.Value(0)).current; // Valor inicial para opacidade: 0
useEffect(() => {
Animated.timing(fadeAnim, {
toValue: 1,
duration: 10000,
useNativeDriver: true,
}).start();
}, [fadeAnim]);
return (
<Animated.View // View especial animável
style={{
...props.style,
opacity: fadeAnim, // Vincular opacidade ao valor animado
}}>
{props.children}
</Animated.View>
);
};
// Você pode então usar seu `FadeInView` no lugar de um `View` em seus componentes:
export default () => {
return (
<View
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}>
<FadeInView
style={{
width: 250,
height: 50,
backgroundColor: 'powderblue',
}}>
<Text style={{fontSize: 28, textAlign: 'center', margin: 10}}>
Fading in
</Text>
</FadeInView>
</View>
);
};
Vamos analisar o que está acontecendo aqui. No construtor FadeInView
, um novo Animated.Value
chamado fadeAnim
é inicializado como parte do estado. A propriedade de opacidade na View
é mapeada para esse valor animado. Nos bastidores, o valor numérico é extraído e usado para definir a opacidade.
Quando o componente é montado, a opacidade é definida como 0. Em seguida, uma animação de atenuação é iniciada no valor animado fadeAnim
, que atualizará todos os seus mapeamentos dependentes (neste caso, apenas a opacidade) em cada quadro à medida que o valor for animado para o valor final de 1.
Isso é feito de maneira otimizada e mais rápida do que chamar setState
e renderizar novamente. Como toda a configuração é declarativa, poderemos implementar otimizações adicionais que serializam a configuração e executam a animação em um thread de alta prioridade.
Configurando animações
As animações são altamente configuráveis. Funções de atenuação personalizadas e predefinidas, atrasos, durações, fatores de decaimento, constantes de mola e muito mais podem ser ajustados dependendo do tipo de animação.
Animated
fornece vários tipos de animação, sendo o mais comumente usado Animated.timing()
. Ele suporta a animação de um valor ao longo do tempo usando uma das várias funções de atenuação predefinidas, ou você pode usar a sua própria. As funções de atenuação são normalmente usadas em animação para transmitir aceleração e desaceleração graduais de objetos.
Por padrão, o tempo usará uma curva easyInOut
que transmite aceleração gradual até a velocidade máxima e termina desacelerando gradualmente até parar. Você pode especificar uma função de atenuação diferente passando um parâmetro de easing. A
durationpersonalizada ou mesmo um
delay` antes do início da animação também é suportada.
Por exemplo, se quisermos criar uma animação de 2 segundos de um objeto que recua ligeiramente antes de se mover para sua posição final:
Animated.timing(this.state.xPosition, {
toValue: 100,
easing: Easing.back(),
duration: 2000,
useNativeDriver: true,
}).start();
Dê uma olhada na seção Configurando animações da referência da API animada para saber mais sobre todos os parâmetros de configuração suportados pelas animações integradas.
Compondo animações
As animações podem ser combinadas e reproduzidas em sequência ou em paralelo. As animações sequenciais podem ser reproduzidas imediatamente após o término da animação anterior ou podem começar após um atraso especificado. A API Animated
fornece vários métodos, como sequence()
e delay()
, cada um dos quais usa uma matriz de animações para ser executado e chama automaticamente start()
/stop()
conforme necessário.
Por exemplo, a animação a seguir para e depois retorna enquanto gira em paralelo:
Animated.sequence([
// decair, então o spring começa e gira
Animated.decay(position, {
// desce até parar
velocity: {x: gestureState.vx, y: gestureState.vy}, // velocidade da liberação do gesto
deceleration: 0.997,
useNativeDriver: true,
}),
Animated.parallel([
// após a decadência, em paralelo:
Animated.spring(position, {
toValue: {x: 0, y: 0}, // voltar para começar
useNativeDriver: true,
}),
Animated.timing(twirl, {
// e girar
toValue: 360,
useNativeDriver: true,
}),
]),
]).start(); // iniciar o grupo de sequência
Se uma animação for parada ou interrompida, todas as outras animações do grupo também serão interrompidas. Animated.parallel
tem uma opção stopTogether
que pode ser definida como false
para desabilitar isso.
Você pode encontrar uma lista completa de métodos de composição na seção Compondo animações da referência da API Animated
.
Combinando valores animados
Você pode combinar dois valores animados por meio de adição, multiplicação, divisão ou módulo para criar um novo valor animado.
Existem alguns casos em que um valor animado precisa inverter outro valor animado para cálculo. Um exemplo é inverter uma escala (2x -> 0,5x):
const a = new Animated.Value(1);
const b = Animated.divide(1, a);
Animated.spring(a, {
toValue: 2,
useNativeDriver: true,
}).start();
Interpolação
Cada propriedade pode ser executada primeiro por meio de uma interpolação. Uma interpolação mapeia intervalos de entrada para intervalos de saída, normalmente usando uma interpolação linear, mas também oferece suporte a funções de atenuação. Por padrão, ele extrapolará a curva além dos intervalos fornecidos, mas você também poderá limitar o valor de saída.
Um mapeamento básico para converter um intervalo de 0-1 em um intervalo de 0-100 seria:
value.interpolate({
inputRange: [0, 1],
outputRange: [0, 100],
});
Por exemplo, você pode querer pensar em seu Animated.Value
como indo de 0 a 1, mas anime a posição de 150px
para 0px
e a opacidade de 0 a 1. Isso pode ser feito modificando o style
do exemplo acima, assim:
style={{
opacity: this.state.fadeAnim, // Vincula diretamente
transform: [{
translateY: this.state.fadeAnim.interpolate({
inputRange: [0, 1],
outputRange: [150, 0] // 0 : 150, 0.5 : 75, 1 : 0
}),
}],
}}
interpolate()
também suporta vários segmentos de intervalo, o que é útil para definir zonas mortas e outros truques úteis. Por exemplo, para obter um relacionamento de negação em -300 que vai para 0 em -100, depois volta para 1 em 0 e depois volta para zero em 100 seguido por uma zona morta que permanece em 0 para tudo além disso, você poderia fazer:
value.interpolate({
inputRange: [-300, -100, 0, 100, 101],
outputRange: [300, 0, 1, 0, 0],
});
O que seria mapeado assim:
Input | Output
------|-------
-400| 450
-300| 300
-200| 150
-100| 0
-50| 0.5
0| 1
50| 0.5
100| 0
101| 0
200| 0
interpolate()
também suporta mapeamento para strings, permitindo animar cores e também valores com unidades. Por exemplo, se você quiser animar uma rotação você poderia fazer:
value.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg'],
});
interpolate()
também suporta funções de atenuação arbitrárias, muitas das quais já estão implementadas no módulo Easing. interpolate()
também possui comportamento configurável para extrapolar o outputRange
. Você pode definir a extrapolação definindo as opções extrapolate
, extrapolateLeft
ou extrapolateRight
. O valor padrão é extend
, mas você pode usar clamp
para evitar que o valor de saída exceda outputRange
.
Rastreando valores dinâmicos
Os valores animados também podem rastrear outros valores definindo toValue
de uma animação como outro valor animado em vez de um número simples. Por exemplo, uma animação "Chat Heads" como a usada pelo Messenger no Android poderia ser implementada com um spring()
fixado em outro valor animado, ou com timing()
e uma duration
de 0 para rastreamento rígido. Também podem ser compostos com interpolações:
Animated.spring(follower, {toValue: leader}).start();
Animated.timing(opacity, {
toValue: pan.x.interpolate({
inputRange: [0, 300],
outputRange: [1, 0],
useNativeDriver: true,
}),
}).start();
Os valores animados do leader
e do follower
seriam implementados usando Animated.ValueXY()
. ValueXY
é uma maneira prática de lidar com interações 2D, como panorâmica ou arrastamento. É um wrapper básico que contém duas instâncias de Animated.Value
e algumas funções auxiliares que as chamam, tornando ValueXY
um substituto imediato para Value
em muitos casos. Isso nos permite rastrear os valores de x e y no exemplo acima.
Gestos de rastreamento
Gestos, como movimento panorâmico ou rolagem, e outros eventos podem ser mapeados diretamente para valores animados usando Animated.event
. Isto é feito com uma sintaxe de mapa estruturada para que os valores possam ser extraídos de objetos de eventos complexos. O primeiro nível é uma matriz para permitir o mapeamento entre vários argumentos, e essa matriz contém objetos aninhados.
Por exemplo, ao trabalhar com gestos de rolagem horizontal, você faria o seguinte para mapear event.nativeEvent.contentOffset.x
para scrollX
(um Animated.Value
):
onScroll={Animated.event(
// scrollX = e.nativeEvent.contentOffset.x
[{nativeEvent: {
contentOffset: {
x: scrollX
}
}
}]
)}
O exemplo a seguir implementa um carrossel de rolagem horizontal onde os indicadores de posição de rolagem são animados usando o Animated.event
usado no ScrollView
ScrollView com exemplo de evento animado
import React, {useRef} from 'react';
import {
SafeAreaView,
ScrollView,
Text,
StyleSheet,
View,
ImageBackground,
Animated,
useWindowDimensions,
} from 'react-native';
const images = new Array(6).fill(
'https://images.unsplash.com/photo-1556740749-887f6717d7e4',
);
const App = () => {
const scrollX = useRef(new Animated.Value(0)).current;
const {width: windowWidth} = useWindowDimensions();
return (
<SafeAreaView style={styles.container}>
<View style={styles.scrollContainer}>
<ScrollView
horizontal={true}
pagingEnabled
showsHorizontalScrollIndicator={false}
onScroll={Animated.event([
{
nativeEvent: {
contentOffset: {
x: scrollX,
},
},
},
])}
scrollEventThrottle={1}>
{images.map((image, imageIndex) => {
return (
<View style={{width: windowWidth, height: 250}} key={imageIndex}>
<ImageBackground source={{uri: image}} style={styles.card}>
<View style={styles.textContainer}>
<Text style={styles.infoText}>
{'Image - ' + imageIndex}
</Text>
</View>
</ImageBackground>
</View>
);
})}
</ScrollView>
<View style={styles.indicatorContainer}>
{images.map((image, imageIndex) => {
const width = scrollX.interpolate({
inputRange: [
windowWidth * (imageIndex - 1),
windowWidth * imageIndex,
windowWidth * (imageIndex + 1),
],
outputRange: [8, 16, 8],
extrapolate: 'clamp',
});
return (
<Animated.View
key={imageIndex}
style={[styles.normalDot, {width}]}
/>
);
})}
</View>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
scrollContainer: {
height: 300,
alignItems: 'center',
justifyContent: 'center',
},
card: {
flex: 1,
marginVertical: 4,
marginHorizontal: 16,
borderRadius: 5,
overflow: 'hidden',
alignItems: 'center',
justifyContent: 'center',
},
textContainer: {
backgroundColor: 'rgba(0,0,0, 0.7)',
paddingHorizontal: 24,
paddingVertical: 8,
borderRadius: 5,
},
infoText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
normalDot: {
height: 8,
width: 8,
borderRadius: 4,
backgroundColor: 'silver',
marginHorizontal: 4,
},
indicatorContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
});
export default App;
Ao usar o PanResponder
, você pode usar o código a seguir para extrair as posições x e y de gestureState.dx
e gestureState.dy
. Usamos um null
na primeira posição do array, pois estamos interessados apenas no segundo argumento passado para o manipulador PanResponder
, que é o gestureState
.
onPanResponderMove={Animated.event(
[null, // ignora o evento nativo
// extraia dx e dy do gestureState
// como 'pan.x = gestureState.dx, pan.y = gestureState.dy'
{dx: pan.x, dy: pan.y}
])}
PanResponder com exemplo de evento animado
import React, {useRef} from 'react';
import {Animated, View, StyleSheet, PanResponder, Text} from 'react-native';
const App = () => {
const pan = useRef(new Animated.ValueXY()).current;
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([null, {dx: pan.x, dy: pan.y}]),
onPanResponderRelease: () => {
Animated.spring(pan, {
toValue: {x: 0, y: 0},
useNativeDriver: true,
}).start();
},
}),
).current;
return (
<View style={styles.container}>
<Text style={styles.titleText}>Drag & Release this box!</Text>
<Animated.View
style={{
transform: [{translateX: pan.x}, {translateY: pan.y}],
}}
{...panResponder.panHandlers}>
<View style={styles.box} />
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
titleText: {
fontSize: 14,
lineHeight: 24,
fontWeight: 'bold',
},
box: {
height: 150,
width: 150,
backgroundColor: 'blue',
borderRadius: 5,
},
});
export default App;
Respondendo ao valor atual da animação
Você pode perceber que não há uma maneira clara de ler o valor atual durante a animação. Isso ocorre porque o valor só pode ser conhecido no tempo de execução nativo devido a otimizações. Se você precisar executar JavaScript em resposta ao valor atual, existem duas abordagens:
spring.stopAnimation(callback)
interromperá a animação e invocará o retorno de chamada com o valor final. Isto é útil ao fazer transições de gestos.spring.addListener(callback)
invocará o retorno de chamada de forma assíncrona enquanto a animação estiver em execução, fornecendo um valor recente. Isso é útil para acionar mudanças de estado, por exemplo, encaixar um bobble em uma nova opção conforme o usuário o arrasta para mais perto, porque essas mudanças de estado maiores são menos sensíveis a alguns quadros de atraso em comparação com gestos contínuos como o movimento panorâmico, que precisa ser executado a 60 fps.
Animated
foi projetado para ser totalmente serializável para que as animações possam ser executadas com alto desempenho, independentemente do loop de eventos normal do JavaScript. Isso influencia a API, então tenha isso em mente quando parecer um pouco mais complicado fazer algo em comparação com um sistema totalmente síncrono. Confira Animated.Value.addListener
como uma forma de contornar algumas dessas limitações, mas use-o com moderação, pois pode ter implicações de desempenho no futuro.
Usando o driver nativo
A API Animated
foi projetada para ser serializável. Ao usar o driver nativo, enviamos tudo sobre a animação para o nativo antes de iniciá-la, permitindo que o código nativo execute a animação no thread da UI sem ter que passar pela ponte em cada quadro. Depois que a animação for iniciada, o thread JS poderá ser bloqueado sem afetar a animação.
O uso do driver nativo para animações normais pode ser feito definindo useNativeDriver: true
na configuração da animação ao iniciá-la. Animações sem uma propriedade useNativeDriver
serão padronizadas como false por motivos legados, mas emitirão um aviso (e erro de verificação de digitação no TypeScript).
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: true, // <-- Seta isso como true
}).start();
Os valores animados são compatíveis apenas com um driver, portanto, se você usar o driver nativo ao iniciar uma animação em um valor, certifique-se de que cada animação nesse valor também use o driver nativo.
O driver nativo também funciona com Animated.event
. Isso é especialmente útil para animações que seguem a posição de rolagem, pois sem o driver nativo, a animação sempre executará um quadro atrás do gesto devido à natureza assíncrona do React Native.
<Animated.ScrollView // <-- Use o wrapper ScrollView animado
scrollEventThrottle={1} // <--Use 1 aqui para garantir que nenhum evento seja perdido
onScroll={Animated.event(
[
{
nativeEvent: {
contentOffset: {y: this.state.animatedValue},
},
},
],
{useNativeDriver: true}, // <-- Seta isso como true
)}>
{content}
</Animated.ScrollView>
Você pode ver o driver nativo em ação executando o aplicativo RNTester e carregando o exemplo animado nativo. Você também pode dar uma olhada no código-fonte para saber como esses exemplos foram produzidos.
Ressalvas
Nem tudo o que você pode fazer com o Animated
é atualmente compatível com o driver nativo. A principal limitação é que você só pode animar propriedades que não sejam de layout: coisas como transformation
e opacity
funcionarão, mas Flexbox e propriedades de posição não. Ao usar Animated.event
, ele funcionará apenas com eventos diretos e não com eventos "borbulhantes". Isso significa que ele não funciona com PanResponder
, mas funciona com coisas como ScrollView#onScroll
.
Quando uma animação está em execução, ela pode impedir que os componentes VirtualizedList
renderizem mais linhas. Se você precisar executar uma animação longa ou em loop enquanto o usuário percorre uma lista, você pode usar isInteraction: false
na configuração da sua animação para evitar esse problema.
Tenha em mente
Ao usar estilos de transformação, como rotateY
, rotateX
e outros, certifique-se de que a perspectiva do estilo de transformação esteja em vigor. No momento, algumas animações podem não ser renderizadas no Android sem ele. Exemplo abaixo.
<Animated.View
style={{
transform: [
{scale: this.state.scale},
{rotateY: this.state.rotateY},
{perspective: 1000}, // sem esta linha esta animação não será renderizada no Android enquanto funciona bem no iOS
],
}}
/>
Exemplos adicionais
O aplicativo RNTester possui vários exemplos de Animated
em uso:
API LayoutAnimation
LayoutAnimation
permite configurar globalmente, criar e atualizar animações que serão usadas para todas as visualizações no próximo ciclo de renderização/layout. Isto é útil para fazer atualizações de layout do Flexbox sem se preocupar em medir ou calcular propriedades específicas para animá-las diretamente, e é especialmente útil quando alterações de layout podem afetar ancestrais, por exemplo, uma expansão "veja mais" que também aumenta o tamanho do pai e empurra para baixo a linha abaixo, o que de outra forma exigiria coordenação explícita entre os componentes para animá-los todos em sincronia.
Observe que, embora o LayoutAnimation
seja muito poderoso e possa ser bastante útil, ele fornece muito menos controle do que o Animated
e outras bibliotecas de animação, portanto, talvez seja necessário usar outra abordagem se não conseguir que o LayoutAnimation
faça o que deseja.
Observe que para que isso funcione no Android, você precisa definir os seguintes sinalizadores via UIManager
:
UIManager.setLayoutAnimationEnabledExperimental(true);
import React from 'react';
import {
NativeModules,
LayoutAnimation,
Text,
TouchableOpacity,
StyleSheet,
View,
} from 'react-native';
const {UIManager} = NativeModules;
UIManager.setLayoutAnimationEnabledExperimental &&
UIManager.setLayoutAnimationEnabledExperimental(true);
export default class App extends React.Component {
state = {
w: 100,
h: 100,
};
_onPress = () => {
// Animate the update
LayoutAnimation.spring();
this.setState({w: this.state.w + 15, h: this.state.h + 15});
};
render() {
return (
<View style={styles.container}>
<View
style={[styles.box, {width: this.state.w, height: this.state.h}]}
/>
<TouchableOpacity onPress={this._onPress}>
<View style={styles.button}>
<Text style={styles.buttonText}>Press me!</Text>
</View>
</TouchableOpacity>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
box: {
width: 200,
height: 200,
backgroundColor: 'red',
},
button: {
backgroundColor: 'black',
paddingHorizontal: 20,
paddingVertical: 15,
marginTop: 15,
},
buttonText: {
color: '#fff',
fontWeight: 'bold',
},
});
Este exemplo usa um valor predefinido, você pode personalizar as animações conforme necessário, consulte LayoutAnimation.js para obter mais informações.
Notas Adicionais
requestAnimationFrame
requestAnimationFrame
é um polyfill do navegador com o qual você deve estar familiarizado. Ele aceita uma função como seu único argumento e chama essa função antes da próxima repintura. É um elemento essencial para animações que sustenta todas as APIs de animação baseadas em JavaScript. Em geral, você não precisa chamar isso sozinho - as APIs de animação gerenciarão as atualizações de quadros para você.
setNativeProps
Conforme mencionado na seção Manipulação Direta, setNativeProps
nos permite modificar propriedades de componentes com suporte nativo (componentes que são realmente apoiados por visualizações nativas, ao contrário dos componentes compostos) diretamente, sem ter que setState
renderizar novamente a hierarquia de componentes.
Poderíamos usar isso no exemplo Rebound para atualizar a escala - isso pode ser útil se o componente que estamos atualizando estiver profundamente aninhado e não tiver sido otimizado com shouldComponentUpdate
.
Se você encontrar suas animações com queda de quadros (desempenho abaixo de 60 quadros por segundo), use setNativeProps
ou shouldComponentUpdate
para otimizá-las. Ou você pode executar as animações no thread da UI em vez do thread JavaScript com a opção useNativeDriver
. Você também pode adiar qualquer trabalho computacional intensivo até que as animações sejam concluídas, usando o InteractionManager
. Você pode monitorar a taxa de quadros usando a ferramenta "FPS Monitor" do menu de desenvolvimento do aplicativo.