Implementando la animación de carga de la app de Twitter en React Native
Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
La app de iOS de Twitter tiene una animación de carga que me gusta bastante.
Cuando la app está lista, el logo de Twitter se expande encantadoramente, revelando la aplicación.
Quería descubrir cómo recrear esta animación de carga con React Native.
Para entender cómo construirla, primero tuve que comprender las distintas partes de la animación. La mejor manera de apreciar los detalles es verla en cámara lenta.
Hay algunos elementos principales que necesitaremos descifrar cómo construir.
-
Escalar el pájaro.
-
Mientras el pájaro crece, mostrar la aplicación subyacente
-
Reducir ligeramente la escala de la aplicación al final
Me tomó bastante tiempo descubrir cómo hacer esta animación.
Empecé con una suposición incorrecta: que el fondo azul y el pájaro de Twitter eran una capa encima de la aplicación y que al crecer el pájaro, se volvía transparente revelando la app debajo. ¡Este enfoque no funciona porque si el pájaro se volviera transparente, mostraría la capa azul, no la aplicación subyacente!
Afortunadamente para ti, querido lector, no tendrás que pasar por la misma frustración que yo. ¡Tienes este tutorial que va directo a lo importante!
La forma correcta
Antes de llegar al código, es importante entender cómo descomponerlo. Para visualizar este efecto, lo recreé en CodePen (incrustado en unos párrafos) para que puedas ver las capas interactivamente.
Hay tres capas principales en este efecto. La primera es la capa de fondo azul. Aunque parece estar encima de la app, en realidad está al fondo.
Luego tenemos una capa blanca lisa. Y finalmente, en primer plano, está nuestra aplicación.
El truco principal de esta animación es usar el logo de Twitter como mask (máscara) que enmascara tanto la app como la capa blanca. No profundizaré mucho en los detalles del enmascaramiento, hay muchos recursos en línea para eso.
Lo básico del enmascaramiento aquí es que los píxeles opacos de la máscara muestran el contenido que enmascaran, mientras que los píxeles transparentes lo ocultan.
Usamos el logo de Twitter como máscara para dos capas: la capa blanca sólida y la capa de la aplicación.
Para revelar la app, escalamos la máscara hasta que sea más grande que toda la pantalla.
Mientras la máscara se escala, aumentamos la opacidad de la capa de la app, mostrándola y ocultando la capa blanca detrás. Para terminar el efecto, iniciamos la capa de la app con escala >1 y la reducimos a 1 al finalizar la animación. Luego ocultamos las capas no-app ya que no se volverán a ver.
Dicen que una imagen vale mil palabras. ¿Cuántas palabras vale una visualización interactiva? Avanza por la animación con el botón "Next Step". Mostrar las capas te da una perspectiva lateral. La cuadrícula ayuda a visualizar las capas transparentes.
Ahora, con React Native
Muy bien. Ahora que sabemos qué construimos y cómo funciona la animación, podemos entrar en código — la verdadera razón por la que estás aquí.
La pieza clave de este rompecabezas es MaskedViewIOS, un componente central de React Native.
import {MaskedViewIOS} from 'react-native';
<MaskedViewIOS maskElement={<Text>Basic Mask</Text>}>
<View style={{backgroundColor: 'blue'}} />
</MaskedViewIOS>;
MaskedViewIOS recibe las props maskElement y children. Los children son enmascarados por el maskElement. Nota que la máscara no necesita ser una imagen, puede ser cualquier vista arbitraria. El comportamiento del ejemplo anterior sería renderizar la vista azul, pero solo sería visible donde las palabras "Basic Mask" aparecen desde el maskElement. Acabamos de crear texto azul complejo.
Lo que queremos hacer es renderizar nuestra capa azul, y luego encima renderizar nuestras capas enmascaradas (la app y la blanca) con el logo de Twitter.
{
fullScreenBlueLayer;
}
<MaskedViewIOS
style={{flex: 1}}
maskElement={
<View style={styles.centeredFullScreen}>
<Image source={twitterLogo} />
</View>
}>
{fullScreenWhiteLayer}
<View style={{flex: 1}}>
<MyApp />
</View>
</MaskedViewIOS>;
Esto nos dará las capas que vemos a continuación.
Ahora la parte animada
Tenemos todas las piezas necesarias para que esto funcione; el siguiente paso es animarlas. Para que esta animación se sienta fluida, utilizaremos la API Animated de React Native.
Animated nos permite definir nuestras animaciones de manera declarativa en JavaScript. Por defecto, estas animaciones se ejecutan en JavaScript y le dicen a la capa nativa qué cambios hacer en cada fotograma. Aunque JavaScript intentará actualizar la animación en cada fotograma, es probable que no pueda hacerlo lo suficientemente rápido y cause pérdida de fotogramas (jank). ¡No es lo que queremos!
Animated tiene un comportamiento especial para evitar este jank. Tiene una bandera llamada useNativeDriver que envía tu definición de animación desde JavaScript al entorno nativo al inicio de tu animación, permitiendo que el lado nativo procese las actualizaciones sin tener que comunicarse con JavaScript en cada fotograma. La desventaja de useNativeDriver es que solo puedes animar un conjunto específico de propiedades, principalmente transform y opacity. No puedes animar cosas como el color de fondo con useNativeDriver, al menos aún no; iremos agregando más propiedades con el tiempo, y por supuesto siempre puedes enviar un PR para las propiedades que necesites en tu proyecto, beneficiando a toda la comunidad 😀.
Como queremos que esta animación sea fluida, trabajaremos dentro de estas restricciones. Para una mirada más profunda sobre cómo funciona useNativeDriver internamente, consulta nuestra publicación de blog que lo anuncia.
Desglosando nuestra animación
Hay 4 componentes en nuestra animación:
-
Agrandar el pájaro, revelando la app y la capa blanca sólida
-
Hacer aparecer gradualmente la app (fade in)
-
Reducir la escala de la app al final
-
Ocultar la capa blanca y azul cuando termine
Con Animated, hay dos formas principales de definir tu animación. La primera es usando Animated.timing, que te permite especificar exactamente cuánto tiempo durará tu animación, junto con una curva de easing para suavizar el movimiento. El otro enfoque es usar las API basadas en física como Animated.spring. Con Animated.spring, especificas parámetros como la cantidad de fricción y tensión en el resorte, y dejas que la física ejecute tu animación.
Tenemos múltiples animaciones que queremos ejecutar simultáneamente y que están estrechamente relacionadas. Por ejemplo, queremos que la app comience a aparecer gradualmente mientras la máscara está a medio revelar. Debido a esta relación cercana, usaremos Animated.timing con un único Animated.Value.
Animated.Value es un envoltorio alrededor de un valor nativo que Animated usa para conocer el estado de una animación. Normalmente querrás tener solo uno de estos para una animación completa. La mayoría de componentes que usan Animated almacenarán este valor en el estado.
Como estoy pensando en esta animación como pasos que ocurren en diferentes momentos a lo largo de la secuencia completa, comenzaremos nuestro Animated.Value en 0 (representando 0% completado) y terminaremos en 100 (representando 100% completado).
El estado inicial de nuestro componente será el siguiente:
state = {
loadingProgress: new Animated.Value(0),
};
Cuando estemos listos para comenzar la animación, le diremos a Animated que anime este valor hasta 100.
Animated.timing(this.state.loadingProgress, {
toValue: 100,
duration: 1000,
useNativeDriver: true, // This is important!
}).start();
Luego intento calcular una estimación aproximada de las diferentes partes de las animaciones y los valores que deberían tener en distintas etapas de la animación general. A continuación se muestra una tabla con las diferentes partes de la animación y los valores que considero deberían tener en distintos puntos a medida que avanzamos en el tiempo.

La máscara del pájaro de Twitter debe comenzar con escala 1, y se hace más pequeña antes de aumentar rápidamente de tamaño. Así que al 10% de la animación, debería tener un valor de escala de 0.8 antes de aumentar hasta escala 70 al final. Elegir 70 fue bastante arbitrario, la verdad; necesitaba ser lo suficientemente grande para que el pájaro revelara completamente la pantalla y 60 no alcanzaba 😀. Algo interesante de esta parte es que cuanto más alto sea el número, más rápido parecerá crecer porque debe llegar allí en el mismo tiempo. Este número requirió varias pruebas hasta lograr que se viera bien con este logo. Logotipos o dispositivos de diferentes tamaños requerirán que esta escala final sea diferente para garantizar que toda la pantalla quede revelada.
La aplicación debe permanecer opaca durante un tiempo, al menos mientras el logo de Twitter se hace más pequeño. Según la animación oficial, quiero comenzar a mostrarla cuando el pájaro esté a mitad de camino en su aumento y revelarla completamente bastante rápido. Así que al 15% comenzamos a mostrarla, y al 30% de la animación total ya es completamente visible.
La escala de la aplicación comienza en 1.1 y se reduce hasta su escala normal al final de la animación.
Y ahora, en código.
Esencialmente, lo que hicimos anteriormente es mapear los valores del porcentaje de progreso de la animación a los valores de las piezas individuales. Hacemos esto con Animated usando .interpolate. Creamos 3 objetos de estilo diferentes, uno para cada parte de la animación, usando valores interpolados basados en this.state.loadingProgress.
const loadingProgress = this.state.loadingProgress;
const opacityClearToVisible = {
opacity: loadingProgress.interpolate({
inputRange: [0, 15, 30],
outputRange: [0, 0, 1],
extrapolate: 'clamp',
// clamp means when the input is 30-100, output should stay at 1
}),
};
const imageScale = {
transform: [
{
scale: loadingProgress.interpolate({
inputRange: [0, 10, 100],
outputRange: [1, 0.8, 70],
}),
},
],
};
const appScale = {
transform: [
{
scale: loadingProgress.interpolate({
inputRange: [0, 100],
outputRange: [1.1, 1],
}),
},
],
};
Ahora que tenemos estos objetos de estilo, podemos usarlos al renderizar el fragmento de la vista que vimos antes en la publicación. Ten en cuenta que solo Animated.View, Animated.Text y Animated.Image pueden usar objetos de estilo que empleen Animated.Value.
const fullScreenBlueLayer = (
<View style={styles.fullScreenBlueLayer} />
);
const fullScreenWhiteLayer = (
<View style={styles.fullScreenWhiteLayer} />
);
return (
<View style={styles.fullScreen}>
{fullScreenBlueLayer}
<MaskedViewIOS
style={{flex: 1}}
maskElement={
<View style={styles.centeredFullScreen}>
<Animated.Image
style={[styles.maskImageStyle, imageScale]}
source={twitterLogo}
/>
</View>
}>
{fullScreenWhiteLayer}
<Animated.View
style={[opacityClearToVisible, appScale, {flex: 1}]}>
{this.props.children}
</Animated.View>
</MaskedViewIOS>
</View>
);
¡Yay! Ahora tenemos las partes de la animación luciendo como queremos. Solo nos queda limpiar nuestras capas azul y blanca que nunca volverán a verse.
Para saber cuándo podemos limpiarlas, necesitamos saber cuándo se completa la animación. Por suerte, cuando llamamos a Animated.timing, .start recibe un callback opcional que se ejecuta cuando la animación termina.
Animated.timing(this.state.loadingProgress, {
toValue: 100,
duration: 1000,
useNativeDriver: true,
}).start(() => {
this.setState({
animationDone: true,
});
});
Ahora que tenemos un valor en state para saber si hemos terminado con la animación, podemos modificar nuestras capas azul y blanca para que lo utilicen.
const fullScreenBlueLayer = this.state.animationDone ? null : (
<View style={[styles.fullScreenBlueLayer]} />
);
const fullScreenWhiteLayer = this.state.animationDone ? null : (
<View style={[styles.fullScreenWhiteLayer]} />
);
¡Voilà! Nuestra animación ahora funciona y limpiamos las capas no utilizadas una vez que termina. ¡Hemos construido la animación de carga de la app de Twitter!
¡Pero espera, el mío no funciona!
No te preocupes, querido lector. Yo también odio cuando las guías solo dan fragmentos de código sin proporcionar el código completo.
Este componente ha sido publicado en npm y está en GitHub como react-native-mask-loader. Para probarlo en tu teléfono, está disponible en Expo aquí:
Más lecturas / Crédito extra
-
Este gitbook es un gran recurso para aprender más sobre Animated después de leer la documentación de React Native.
-
La animación real de Twitter parece acelerar la revelación de la máscara hacia el final. Intenta modificar el loader para usar una función de easing diferente (¡o un spring!) para igualar mejor ese comportamiento.
-
La escala final actual de la máscara está codificada y probablemente no revelará toda la aplicación en una tablet. Calcular la escala final basada en el tamaño de pantalla y el tamaño de la imagen sería un PR increíble.
