Skip to Content

Chapitre 10.1 — Le paysage des animations RN

Où on en est : Drill fonctionne mais tout apparaît/disparaît sèchement. Avant d’animer, comprendre POURQUOI l’animation mobile a ses règles — sinon Reanimated ressemble à de la magie noire.

⏱️ TL;DR — Une animation doit produire une image toutes les 8-16 ms (60-120 Hz). Le thread JS ne peut pas le garantir (il fait tourner votre app !). Trois générations de réponses : l’API Animated historique (avec useNativeDriver, limitée), Reanimated (le standard : l’animation VIT sur le thread UI via les worklets), et les Layout Animations intégrées pour les cas simples. Notre stack : Reanimated partout, point.

🎯 Objectifs

  • Comprendre la contrainte des 16 ms et pourquoi le thread JS ne peut pas la tenir.
  • Situer Animated (l’historique), Reanimated (le standard), et quand chacun apparaît.
  • Savoir lire du code Animated dans les vieux tutos sans le copier.
  • Vérifier l’installation de Reanimated (déjà là via NativeWind !).

La contrainte : 16 millisecondes

Un écran 60 Hz redessine toutes les 16,6 ms (120 Hz : 8,3 ms). Une animation fluide doit fournir la nouvelle position/opacité/échelle à chaque frame. Ratez quelques frames : ça « saccade » — l’œil est impitoyable.

Or, piloter une animation depuis le thread JS signifie : à chaque frame, le JS calcule la valeur → la passe au natif via JSI → Fabric applique. Problème : le thread JS fait AUSSI tourner React, vos stores, votre logique — qu’un answer() déclenche un enregistrement SQLite au mauvais moment, et la frame est ratée.

Génération 1 : l’API Animated (l’historique)

Le RN d’origine (que vous croiserez dans TOUS les tutos pré-2022) :

// Style historique — À RECONNAÎTRE, pas à écrire const opacity = useRef(new Animated.Value(0)).current Animated.timing(opacity, { toValue: 1, duration: 300, useNativeDriver: true, // ← le patch : déléguer l'exécution au natif }).start() <Animated.View style={{ opacity }} />

useNativeDriver: true envoyait l’animation côté natif… mais seulement pour les propriétés non-layout (opacity, transform) — jamais width, height, ni les couleurs. Et aucune logique dynamique côté natif (réagir à un geste frame par frame = retour au JS). L’API fonctionne toujours ; elle est simplement dépassée.

Génération 2 : Reanimated — la logique déménage

L’idée de rupture : au lieu d’envoyer des valeurs au natif, envoyer du code. Les worklets — de petites fonctions JS compilées pour s’exécuter sur le thread UI :

// Aperçu (le chapitre suivant détaille tout) const scale = useSharedValue(1) const style = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }], // ← ce code TOURNE sur le thread UI })) scale.value = withSpring(0.95) // ← l'animation vit côté natif

Conséquences : TOUTES les propriétés sont animables (layout compris), les gestes se traitent frame par frame sans toucher le thread JS (chapitre 10.4), et un thread JS occupé n’affecte RIEN. Reanimated est le moteur de fait de l’écosystème — les libs d’UI, les navigateurs, les bottom sheets : tout est construit dessus.

Bonus déjà installé : NativeWind dépend de Reanimated (chapitre 4.4) — vérifiez juste babel.config.js : si vous suivez le cours, react-native-reanimated/plugin… n’est plus nécessaire depuis Reanimated 4 en SDK 57 (le plugin worklets est intégré à babel-preset-expo). npx expo-doctor confirme une install saine.

Génération 2,5 : les Layout Animations

Reanimated embarque aussi des animations déclaratives pour les cas ultra-courants (apparition, disparition, réorganisation) :

import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' <Animated.View entering={FadeIn.duration(200)} exiting={FadeOut}> <ExplanationCard /> </Animated.View>

Une prop, zéro shared value — l’explication de Drill apparaîtra comme ça (chapitre 10.5). La hiérarchie de paresse saine : Layout Animations pour entrer/sortir → withTiming/ withSpring pour les transitions d’état → gestes + worklets pour l’interactif.

⚠️ Piège — Les tutos YouTube pré-2022 (Animated + useNativeDriver) et 2022-2023 (Reanimated 2, plugin babel à configurer, syntaxe 'worklet' explicite partout) encombrent les recherches. Repères de fraîcheur : Reanimated 3/4, pas de plugin babel manuel en Expo SDK 57, et l’import react-native-reanimated. En cas de doute : la doc officielle docs.swmansion.com/react-native-reanimated — excellente, avec playgrounds.

💡 Pour un dev Next.js — Vos repères web : les transitions CSS tournent hors du main thread JS (compositor) — Reanimated recrée exactement cette séparation. Framer Motion ≈ Layout Animations + withSpring (l’esprit déclaratif). Et le requestAnimationFrame piloté en JS que vous évitiez sur le web… est précisément ce que Reanimated évite ici.

✏️ Exercices

1. Sans coder : classez ces besoins de Drill par outil (Layout Animation / timing-spring / gestes worklets) — (a) l’explication qui apparaît, (b) la ProgressBar du timer qui se vide continûment, (c) le swipe-to-delete de l’historique, (d) le bouton qui « pop » au tap.

✅ Solution

(a) Layout Animation (entering FadeIn — un montage). (b) timing (une valeur continue sur la durée du timer — ch. 10.3). (c) gestes + worklets (suivre le doigt frame par frame — ch. 10.4). (d) withSpring sur un scale (transition d’état pressed). La hiérarchie de paresse en action.

2. Un tuto de 2021 anime la hauteur d’un accordéon avec Animated et se plaint que useNativeDriver ne marche pas. Expliquez pourquoi, et la réponse 2026.

✅ Solution

height est une propriété de layout : le native driver historique ne gérait que opacity/transform (le layout exige Yoga, resté côté… layout). Le tuto animait donc en JS pur = saccades. Réponse 2026 : Reanimated anime le layout sans restriction (les worklets pilotent aussi Yoga), ou mieux — une Layout Animation de réorganisation fait l’accordéon en une prop.

3. Vérifiez votre installation : un carré qui pulse (scale 1 → 1,1 → 1 en boucle) avec useSharedValue + withRepeat. Si ça tourne, Reanimated est prêt pour la suite.

✅ Solution

const scale = useSharedValue(1) useEffect(() => { scale.value = withRepeat(withTiming(1.1, { duration: 600 }), -1, true) }, []) const style = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] })) <Animated.View style={[{ width: 80, height: 80, backgroundColor: '#087ea4', borderRadius: 16 }, style]} />

withRepeat(…, -1, true) = boucle infinie en yo-yo. Si le carré pulse même pendant un gros console.log en boucle dans un bouton : preuve du thread UI par l’expérience.

🧠 Quiz

1. Pourquoi 16 ms est-il le chiffre magique ?

Réponse

60 Hz = une frame toutes les 16,6 ms : l’animation doit fournir chaque valeur dans ce budget, sinon frame ratée = saccade visible (120 Hz : 8,3 ms — encore plus strict).

2. Quelle est l’idée de rupture de Reanimated ?

Réponse

Envoyer du code (worklets) au thread UI, pas des valeurs : l’animation et sa logique s’exécutent côté natif, insensibles à l’occupation du thread JS.

3. Quelles étaient les deux limites de useNativeDriver ?

Réponse

Propriétés non-layout uniquement (opacity/transform — pas height/width/couleurs), et aucune logique dynamique côté natif (gestes frame par frame impossibles sans repasser par le JS).

4. Qu’est-ce qu’une Layout Animation et quand suffit-elle ?

Réponse

Une animation déclarative d’apparition/disparition/réorganisation (entering={FadeIn}) — suffisante pour tous les montages/démontages simples, zéro shared value.

5. Pourquoi Reanimated est-il déjà installé dans Drill ?

Réponse

NativeWind en dépend (installé en 4.4). En SDK 57/Reanimated 4, plus de plugin babel manuel — babel-preset-expo intègre les worklets.


👉 Chapitre suivant : 10.2 — Reanimated : fondations — shared values, worklets, et vos premières vraies animations.