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é natifConsé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’importreact-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
requestAnimationFramepiloté 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.