• 30 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 09/09/2020

Améliorez votre application avec des animations

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Les animations font partie intégrante des applications mobiles. Elles sont bien sûr optionnelles, mais c'est un plus non négligeable. C'est ce qui rend votre application unique et interactive. Notre application est déjà pleine d'animations, sans que l'on s'en rende compte. Par exemple, quand on passe d'une vue à une autre, il y a une animation pour afficher la nouvelle vue. Quand on clique sur un bouton, il y a une animation pour faire varier son opacité, sa transparence. Quand on affiche un chargement, ce dernier ne tourne pas tout seul, c'est encore une animation.

Parfois, les animations déjà présentes suffisent, parfois, non et on veut en faire plus. On veut créer ses propres interactions et offrir une meilleure expérience utilisateur (UX). On a tendance à penser que les animations sont difficiles à mettre en place, mais je vous rassure, pas du tout ! Une animation n'est, quand on y pense, qu'une valeur qui varie sur une durée. Quand une nouvelle vue s'affiche sur iOS, elle glisse de droite à gauche sur l'écran. Et bien, l'animation derrière, c'est la position X de la vue, qui passe d'une valeur suffisante pour que la vue soit hors de l'écran à 0, et avec une durée de... allez, disons 0,2 seconde. C'est tout. Après, on peut jouer sur la vitesse au début, à la fin, pendant l'animation, mais cela reste une valeur qui varie sur une durée. Retenez bien cela, ce sera utile pour la suite.

Dans ce chapitre, je vais vous présenter les différents types d'animations et comment les réaliser. Vous verrez qu'il n'y a pas de limite si ce n'est votre imagination et... le device parfois, si on va trop loin. Les animations restent un processus lourd qui peut affecter le FPS (Frame Per Second) de votre device. Oui oui, FPS, comme dans les jeux vidéos. Alors, c'est parti, mais pas trop vite quand même. ;)

Animated

Animated est une librairie de React Native permettant de créer des animations. D'après la documentation, les animations de cette librairie sont fluides, puissantes et faciles à faire. On va voir cela tout de suite.

Avant de nous lancer, il faut que je vous présente les deux types de valeurs pour les animations Animated :

  • Animated.Value()  où on définit une valeur, utile lorsqu'on souhaite déplacer un élément sur un seul axe (X ou Y), changer la taille d'un élément, etc. C'est ce qu'on utilisera ici, dans ce chapitre, et c'est ce qui est le plus utilisé.

  • Animated.ValueXY()  où on définit un vecteur, utile pour déplacer un élément sur deux axes.

Avec ces valeurs, on peut définir plusieurs types d'animations AnimatedOn va les découvrir une par une, en les testant à chaque fois. On commence par  Animated.timing()  .

Animated.timing

 Animated.timing()  permet de modifier la valeur (qui affecte votre animation) en spécifiant une durée et une courbe d'accélération

On peut donc jouer sur la manière dont la valeur d'animation évolue au fil du temps. React Native possède une API Easing qui définit les différentes courbes d'animation. Vous pouvez retrouver la liste des courbes sur le guide easing.net. Allez jeter un œil, on peut tester le rendu de chaque courbe d'animation, c'est vraiment bien fait.

Passons à la pratique. Avant de créer notre animation, dans le component Test, on va créer une vue plus basique avec un carré rouge de 100 par 100 centré au milieu. Comme ceci :

// Components/Test.js
import React from 'react'
import { StyleSheet, View } from 'react-native'
class Test extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<View style={styles.main_container}>
<View style={styles.animation_view}>
</View>
</View>
)
}
}
const styles = StyleSheet.create({
main_container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
animation_view: {
backgroundColor: 'red',
width: 100,
height: 100
}
})
export default Test

Pour cette animation, on va faire simple. Je veux que mon carré rouge s'anime et descende de 100 vers le bas. En langage plus technique, cela signifie que, pendant l'animation, il faut que le style  top  de notre carré rouge passe de 0 à 100.

On va donc créer une Animated.Valueinitialiser à 0 et on va l'appliquer au style  top  de notre carré rouge : 

// Components/Test.js
import { ..., Animated } from 'react-native'
class Test extends React.Component {
constructor(props) {
super(props)
this.state = {
topPosition: new Animated.Value(0)
}
}
render() {
return (
<View style={styles.main_container}>
<Animated.View style={[styles.animation_view, { top: this.state.topPosition }]}>
</Animated.View>
</View>
)
}
}

À présent, passons à la création de notre animation, c'est-à-dire à la fonctionnalité qui fait varier notre Animated.Value de 0 à 100. Pour la durée de l'animation, on va mettre 3 secondes. On va lancer notre animation dès que le component Test a fini d'être rendu, donc dans la méthode  componentDidMount  : 

// Components/Test.js
import { ..., Easing } from 'react-native'
//...
componentDidMount() {
Animated.timing(
this.state.topPosition,
{
toValue: 100,
duration: 3000, // Le temps est en milliseconds ici (3000ms = 3sec)
easing: Easing.linear,
}
).start() // N'oubliez pas de lancer votre animation avec la fonction start()
}

Les types d'animations prennent en paramètre une valeur et une configuration :  (value, config) .

  • Dans la valeur, on définit l'Animated.Value qui va varier durant l'animation.

  • La configuration varie selon les types d'animations. Pour Animated.timing, on définit les informations telles que la valeur à atteindre  toValue , le temps de l'animation  duration  et la courbe d'animation  easing . Ici, j'ai choisi  Easing.linear  , la vitesse de mon animation sera donc constante.

Bon, par contre, pour vous montrer le résultat, cela va être compliqué. :lol: À part vous montrer le début de l'animation et la fin, je ne peux pas faire mieux. Mais allez-y, testez sur votre device et rechargez votre application pour revoir l'animation :

Animated.timing avant / après l'animation
Animated.timing avant / après l'animation

Amusez-vous à changer la courbe d'accélération pour voir les différences :  Easing.back()  ,  Easing.elastic(2)  ,  Easing.bounce  , etc. Plutôt sympa, non ?

Animated.spring

 Animated.spring()  permet de modifier la valeur (qui affecte votre animation) selon une vitesse et une élasticité. Pour cette animation, il faut s'imaginer un objet au bout d'un élastique qu'on lâche. Votre objet va tomber, étirer l'élastique, remonter, redescendre, etc., jusqu'à se stabiliser. Rien de mieux qu'un exemple pour bien comprendre : 

// Components/Test.js
componentDidMount() {
Animated.spring(
this.state.topPosition,
{
toValue: 100,
speed: 4,
bounciness: 30
}
).start();
}

Allez-y, testez-la ! Plutôt marrante, celle-ci, n'est-ce pas ? On peut définir une Animated.spring de deux manières différentes. Au lieu de définir une vitesse (  speed  ) et une élasticité (  bounciness  ), on peut définir une tension  tension  et une force de frottement  friction. Cela rappelle les cours de physique ? :lol:  

Animated.decay

Animated.decay()  fonctionne différemment des 2 premiers types d'animation que l'on a vu. Au lieu de définir un point A d'où partir et un point B à atteindre, on va définir uniquement un point A, une vitesse de départ et une décélération. Ici, c'est comme si on lançait un objet et qu'on laissait les forces de frottement l'arrêter. Essayez sur notre application : 

// Components/Test.js
componentDidMount() {
Animated.decay(
this.state.topPosition,
{
velocity: 0.8,
deceleration: 0.997,
}
).start();
}

Cette dernière animation n'est pas facile à gérer. Ce type d'animation est utilisé dans les listes de données FlatList, comme notre liste de films. Lorsque l'utilisateur scrolle dans notre liste, dès qu'il relâche son doigt, la liste continue à scroller, jusqu'à s'arrêter lentement. C'est une Animated.decay. La vitesse de départ est la vitesse qu'a votre liste quand l'utilisateur relâche son doigt et la décélération. C'est le paramètre qui fait décroître la vitesse de la liste au fil du temps. Sans cette animation, dès que l'utilisateur relâcherait son doigt, le défilement de la liste s'arrêterait. Pas terrible, comme comportement. 

Combinez des animations

En React Native, il est possible de combiner les animations. Vous pouvez jouer des animations les unes à la suite des autres et même jouer des animations en même temps.

Animated.sequence

Animated.sequence()  permet de jouer des animations les unes à la suite des autres, comme une séquence, en fait. :p Pour notre exemple, nous allons jouer une animation  spring  , qui va déplacer notre carré rouge vers le bas de 100. Puis, nous allons jouer une animation  timing  qui va replacer le carré rouge à sa place d'origine : 

// Components/Test.js
componentDidMount() {
Animated.sequence([
Animated.spring(
this.state.topPosition,
{
toValue: 100,
tension: 8,
friction: 3
}
),
Animated.timing(
this.state.topPosition,
{
toValue: 0,
duration: 1000,
easing: Easing.elastic(2)
}
)
]).start()
}

Encore une fois, je vous invite à tester pour vous rendre compte du résultat par vous-même. Ce n'est pas fou, mais c'est un exemple. Ici, j'ai enchaîné deux animations, mais, bien sûr, vous pouvez en enchaîner autant que souhaité.

Animated.parallel

Animated.parallel()  permet de jouer des animations en même temps. Pour cet exemple, on va devoir créer une deuxième Animated.Value. Vous ne pouvez pas modifier la même valeur en même temps sur deux animations différentes. C'est plutôt logique. Si vous le faites, votre Animated.parallel ne va prendre en compte que la deuxième animation. 

Je vous propose deux animations en parallèle avec :

  • Une animation spring qui descend notre carré rouge de 100 vers le bas

  • Une animation timing qui décale notre carré rouge de 100 vers la droite

Jouer en même temps, cela devrait nous donner un truc sympa. ^^ Côté code, cela nous donne :

// Components/Test.js
class Test extends React.Component {
constructor(props) {
super(props)
this.state = {
topPosition: new Animated.Value(0),
leftPosition: new Animated.Value(0),
}
}
componentDidMount() {
Animated.parallel([
Animated.spring(
this.state.topPosition,
{
toValue: 100,
tension: 8,
friction: 3
}
),
Animated.timing(
this.state.leftPosition,
{
toValue: 100,
duration: 1000,
easing: Easing.elastic(2)
}
)
]).start()
}
render() {
return (
<View style={styles.main_container}>
<Animated.View style={[styles.animation_view, { top: this.state.topPosition, left: this.state.leftPosition }]}></Animated.View>
</View>
)
}
}

Cette fois, vous devriez voir notre carré rouge faire une jolie boucle pour finir en position 100/100, comme ceci :

Animation spring et timing en parallèle avant / après
Animation spring et timing en parallèle avant / après

Et voilà, vous connaissez tout sur l'API Animated de React Native. Finalement, il n'y a pas grand-chose, mais c'est amplement suffisant pour réaliser de nombreuses animations. Vous verrez que, même avec beaucoup d'imagination, l'API Animated répondra souvent à vos besoins.

PanResponder

L'API PanResponder n'est pas vraiment liée aux animations, mais elle s'en rapproche. L'API permet de détecter les gestes de l'utilisateur à l'écran et d'effectuer des actions en fonction.

Tout à l'heure, je vous ai parlé de l'animation decay et de son implication dans les listes de données FlatList. Mais je ne vous ai pas parlé de ce qu'il se passe avant. Lorsque vous scrollez dans la liste de données, vous créez un geste. Il y a bien quelque chose, un élément qui détecte ce geste, la vitesse de votre geste, sa direction, etc. Et bien, ce quelque chose, c'est un PanResponderIl détecte le début de votre geste jusqu'à sa fin et, dans mon exemple, lorsque le geste de défilement est fini sur la liste, c'est lui, le PanResponder, qui lance l'animation decay. 

Un PanResponder va donc détecter les gestes de l'utilisateur et effectuer des actions en fonction de ces gestes. C'est assez facile à mettre en place. On va reprendre l'exemple de notre carré rouge et on va créer un PanResponder qui va déplacer le carré en suivant les gestes de l'utilisateur.

Je vous mets le code :

// Components/Tests.js
import { ..., PanResponder, Dimensions } from 'react-native'
class Test extends React.Component {
constructor(props) {
super(props)
this.state = {
topPosition: 0,
leftPosition: 0,
}
var {height, width} = Dimensions.get('window');
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderMove: (evt, gestureState) => {
let touches = evt.nativeEvent.touches;
if (touches.length == 1) {
this.setState({
topPosition: touches[0].pageY - height/2,
leftPosition: touches[0].pageX - width/2
})
}
}
})
}
render() {
return (
<View style={styles.main_container}>
<View
{...this.panResponder.panHandlers}
style={[styles.animation_view, { top: this.state.topPosition, left: this.state.leftPosition }]}>
</View>
</View>
)
}
}

 Qu'est-ce que t'as fait ici ? Je n'ai pas tout compris ?

Première chose, dans le  constructor  , j'ai créé un PanResponder avec deux propriétés : 

  1. onStartShouldSetPanResponder  : qui indique au PanResponder s'il doit détecter les gestes dès son initialisation. On a mis  true  . Indispensable, sans quoi le PanResponder ne détecterait rien.

  2. onPanResponderMove  : qui définit l'action à effectuer à chaque geste de l'utilisateur.  

Pour l'action ici, je récupère les touches  , autrement dit les doigts posés sur l'écran. L'utilisateur peut poser un, deux, trois... doigts en même temps sur l'écran. Par exemple, lorsque vous cliquez sur un bouton, scrollez dans une liste, vous utilisez un doigt, donc un  touche . Mais lorsque vous zoomez, dézoomez, vous utilisez deux doigts, donc deux  touches. C'est pour cela que  touches  est un tableau.  

Nous ne souhaitons gérer les gestes que s'il y a un doigt posé, d'où le  if (touches.length == 1) .

Ensuite, et bien, ce sont des maths. ^^

Mon carré rouge est centré par défaut dans la vue, sa position vaut 0/0. Mais mon touch se base sur la position de mon doigt sur l'écran tout entier. On est donc dans deux échelles différentes ici. Si j'appuie au milieu de l'écran, la position ne vaut pas 0/0, mais, par exemple, 200/400 si l'écran fait 400 de largeur sur 800 de haut. Si je souhaite bouger mon carré rouge correctement, dans un environnement X/Y où 0/0 correspond au centre de l'écran, il faut soustraire aux coordonnées de mon  touch  la moitié de la taille de l'écran. C'est ce que j'ai fait ici :

topPosition: touches[0].pageY - height/2,
leftPosition: touches[0].pageX - width/2

J'ai utilisé l'API Dimensions  de React Native pour récupérer la taille de l'écran.

Ah et vous avez testé notre PanResponder ? On peut jouer avec et placer le carré où bon nous semble. Cela vous donne des idées d'applications ? :)

PanResponder iOS / Android où on déplace le carré rouge où on veut
PanResponder iOS / Android où on déplace le carré rouge où on veut

On a bien joué avec les animations et les PanResponders. Vous avez en main un panel d'outils conséquent pour mettre en place des animations vraiment sympas dans vos applications. Avant d'en finir avec les animations, on va pratiquer encore un peu et ajouter des animations dans notre application de gestion de films, pour la rendre... unique:soleil:

Ajoutez une animation sur les FilmItem

Pour commencer, je vous propose de créer une animation sur les items de notre liste de films, les FilmItem. Au lieu de les faire apparaître tout bêtement, on va les afficher avec une translation, de droite à gauche. En gros, nos items vont être créés en dehors de l'écran et glisser de droite à gauche jusqu'à se positionner correctement. On va utiliser une animation de type spring  Animated.spring()  pour avoir un joli effet de ressort.

Normalement, avec tout ce que l'on a vu, vous devriez être en mesure de le faire assez facilement, comme ceci : (Ne le faites pas, c'est juste pour la démonstration.)

// Components/FilmItem.js
import { ..., Animated, Dimensions } from 'react-native'
constructor(props) {
super(props)
this.state = {
positionLeft: new Animated.Value(Dimensions.get('window').width)
}
}
componentDidMount() {
Animated.spring(
this.state.positionLeft,
{
toValue: 0
}
).start()
}
render() {
const { film, displayDetailForFilm } = this.props
return (
<Animated.View
style={{ left: this.state.positionLeft }}
<TouchableOpacity
//...
</TouchableOpacity>
</Animated.View>
)
}

Pas de grande surprise ici. On a encapsulé le rendu de notre item dans une Animated.View à laquelle on a appliqué la valeur de notre animation Animated.Value. Puis, à la création de votre component, dans le  componentDidMount() , on joue l'animation spring. La seule particularité ici est que l'on initialise  positionLeft  avec la largeur de l'écran pour initialiser notre item en dehors de l'écran et que l'animation part de ce point pour revenir en position 0. 

OK, on a fait l'animation, mais pourquoi ne veux-tu pas que l'on ajoute ce code dans l'application ?

En fait, cette animation me plaît bien. :) Elle est certes classique, mais j'aimerais l'utiliser sur d'autres components, d'autres vues. Actuellement, on ne peut pas, l'animation est créée et appliquée directement dans le component FilmItem.

Si on souhaite utiliser cette animation sur d'autres éléments, il faut la "sortir" / l'externaliser et en créer un component à part entière. C'est exactement ce que l'on va faire.

Externalisez une animation

Commençons par créer un dossier /Animations spécialement pour nos animations à la racine du projet. Puis, créer un fichier FadeIn.js et son component FadeInOn va créer, dans le component FadeIn, la même animation que l'on a créée, pour l'exemple, dans le component FilmItem. Cela nous donne :

// Animations/FadeIn.js
import React from 'react'
import { Animated, Dimensions } from 'react-native'
class FadeIn extends React.Component {
constructor(props) {
super(props)
this.state = {
positionLeft: new Animated.Value(Dimensions.get('window').width)
}
}
componentDidMount() {
Animated.spring(
this.state.positionLeft,
{
toValue: 0
}
).start()
}
render() {
return (
<Animated.View
style={{ left: this.state.positionLeft }}>
</Animated.View>
)
}
}
export default FadeIn

Arrêtez-moi si je me trompe, mais là encore, rien de surprenant. On a juste refait l'animation précédente, mais cette fois, dans un component à part entière.

On va maintenant appliquer notre toute nouvelle animation, bien externalisée, prête à être utilisée n'importe où, dans le component FilmItem :

// Components/FilmItem.js
import FadeIn from '../Animations/FadeIn'
//...
render() {
const { film, displayDetailForFilm } = this.props
return (
<FadeIn>
<TouchableOpacity
//...
</TouchableOpacity>
</FadeIn>
)
}

Rendez-vous côté application. Faites une recherche et... :

Externalisation d'une animation, tentative 1
Externalisation d'une animation, tentative 1

Les films n'apparaissent plus. On a tout pété. o_O

Une idée ?

Vous n'avez pas remarqué une différence dans la manière dont on a ajouté notre component animation FadeIn ? 

On n'a pas fait :

return (
<FadeIn/>
<TouchableOpacity
//...
</TouchableOpacity>
)

Mais bien :

return (
<FadeIn>
<TouchableOpacity
//...
</TouchableOpacity>
</FadeIn>
)

Ici, on a encapsulé toute notre vue dans notre component custom FadeIn. C'est la première fois que l'on fait cela. Définit tel quel, le component FadeIn est un component parent, et le component TouchableOpacity, et toute notre vue restante, sont ses components enfants

Si vous regardez bien le code, notre component parent FadeIn ne renvoie qu'une Animated.View. Aucune trace des components enfants ! Et c'est bien là le problème. Quand React Native et son fameux JSX vont remplacer votre  render  par son équivalent en Javascript, ils vont renvoyer uniquement l'Animated.View, sans les components enfants. Les components enfants ne sont alors pas rendus et ne s'affichent pas. C'est ce qu'il se passe dans notre application.

Je sais que ce n'est pas facile à s'imaginer. Comment des components, que l'on a pourtant bien ajoutés au  render  du component FilmItem, peuvent ne pas être rendus ? Juste parce que l'on a ajouté un component parent qui englobe notre item ?

C'est un des rares inconvénients de JSX, selon moi, bien sûr. ^^ JSX simplifie la création de vos éléments graphiques, si bien qu'on passe à côté de certains comportements, comme le fait qu'un component parent doive renvoyer ses components enfants. C'est comme cela que toute l'architecture des vues en React Native fonctionne. Par exemple, ici :

<View>
<Button .../>
</View>

Le component View est un component parent et, même si on ne le voit pas, il renvoie ses components enfants : le bouton ici. 

Il faut donc renvoyer les components enfants, j'ai compris, mais comment fait-on ?

Vous ne pouvez pas le deviner et, moi non plus, d'ailleurs. Le sujet est abordé dans un chapitre de la documentation de React.JS.

En fait, lorsque vous ajoutez des components enfants dans un component parent, comme ici avec notre component parent FadeIn, les components enfants sont transmis, via les props, au component parent. Cela fait partie des choses que l'on ne voit toujours pas et qui sont entièrement gérées par React Native et JSX. Tous les components enfants sont transmis, plus spécifiquement, dans la propriété  children  des props du component parent. On peut donc rendre et afficher les components enfants en ajoutant  this.props.children  dans le rendu du component parent, comme ceci : 

// Animations/FadeIn.js
render() {
return (
<Animated.View
style={{ left: this.state.positionLeft }}>
{this.props.children}
</Animated.View>
)
}

Vous voyez ? Vous ne risquiez pas de le deviner. Cela fait partie des pièges dans lesquels on tombe au moins une fois en React Native et qu'il faut connaître. La prochaine fois, vous saurez pourquoi. ;)

À présent, si vous retournez sur l'application, faites une recherche. Vous devriez voir apparaître vos films avec une animation de droite à gauche, pour terminer sur :

Liste des films une fois l'animation terminée
Liste des films une fois l'animation terminée

Bravo ! Vous avez créé et externalisé une animation. À présent, si vous voulez ajouter cette animation sur un autre de vos components, vous n'aurez qu'à entourer son rendu par un component parent FadeIn. Super pratique et facile. :magicien:

Allez, encore une dernière animation et je vous laisse tranquille avec ça. On va ajouter une animation sur le 🖤permettant d'ajouter / supprimer un film des favoris.

Ajoutez un agrandissement / rétrécissement sur le bouton Favoris

L'animation est plutôt simple :

  1. Quand on ajoute un film aux favoris, le bouton d'ajout passe de ♡ à 🖤, ça, ça marche déjà. Maintenant, je souhaite, qu'en plus, le 🖤double de volume, qu'il passe d'une taille de 40x40 à 80x80 avec une animation spring.

  2. Quand on supprime un film des favoris, le bouton de suppression passe de 🖤à ♡, ça aussi c'est OK. Maintenant, je souhaite, qu'en plus, le 🖤diminue par 2 son volume, qu'il passe de 80x80 à 40x40 avec une animation spring.

  3. Enfin, je veux que toute l'animation soit externalisée dans un component à part entière, comme on l'a fait pour l'animation FadeIn.

Vous vous en sentez capable ? Et bien, c'est à vous de jouer. :) Voici le rendu final attendu :

Animation du bouton favoris : Agrandissement / Rétrécissement
Animation du bouton Favoris : Agrandissement / Rétrécissement

Mais attendez encore un peu. Je ne vous laisse pas tout seul, je vous donne quelques indications pour que l'on parte tous sur les mêmes bases :

  •  Il faut créer une animation, que j'ai nommée EnlargeShrink, qui prend, via les props, un booléen  shouldEnlarge  en fonction de si le 🖤doit être agrandi ou rétréci.

  • Il faut jouer l'animation quand le 🖤est mis à jour, c'est-à-dire quand le component FilmDetail et tout ce qu'il contient sont re-rendus. Reportez-vous aux cycles de vie pour connaître la méthode à surcharger.

  • Pensez également à gérer le cas où l'utilisateur affiche le détail d'un film déjà en favoris. Dans ce cas précis, le 🖤doit être, par défaut, agrandi.

  • Faites attention également au style que l'on a défini sur l'image du 🖤. Il faut indiquer à notre image d'adapter sa taille à l'espace disponible. Pour les images, c'est un peu particulier à réaliser, alors je vous donne l'astuce. Remplacez le style  favorite_image  par  : 

    favorite_image:{
    flex: 1,
    width: null,
    height: null
    }

Allez, cette fois, c'est à vous. Respirez un grand coup et lancez-vous ! ;)

Voici la solution :

// Animations/EnlargeShrink.js
import React from 'react'
import { Animated } from 'react-native'
class EnlargeShrink extends React.Component {
constructor(props) {
super(props)
this.state = {
viewSize: new Animated.Value(this._getSize())
}
}
_getSize() {
if (this.props.shouldEnlarge) {
return 80
}
return 40
}
// La méthode componentDidUpdate est exécuté chaque fois que le component est mise à jour, c'est l'endroit parfait pour lancer / relancer notre animation.
componentDidUpdate() {
Animated.spring(
this.state.viewSize,
{
toValue: this._getSize()
}
).start()
}
render() {
return (
<Animated.View
style={{ width: this.state.viewSize, height: this.state.viewSize }}>
{this.props.children}
</Animated.View>
)
}
}
export default EnlargeShrink
// Components/FilmDetail.js
import React from 'react'
import { StyleSheet, View, Text, ActivityIndicator, ScrollView, Image, TouchableOpacity, Share, Alert, Platform, Button } from 'react-native'
import { getFilmDetailFromApi, getImageFromApi } from '../API/TMDBApi'
import moment from 'moment'
import numeral from 'numeral'
import { connect } from 'react-redux'
import EnlargeShrink from '../Animations/EnlargeShrink'
class FilmDetail extends React.Component {
static navigationOptions = ({ navigation }) => {
const { params } = navigation.state
if (params.film != undefined && Platform.OS === 'ios') {
return {
headerRight: <TouchableOpacity
style={styles.share_touchable_headerrightbutton}
onPress={() => params.shareFilm()}>
<Image
style={styles.share_image}
source={require('../Images/ic_share.png')} />
</TouchableOpacity>
}
}
}
constructor(props) {
super(props)
this.state = {
film: undefined,
isLoading: false
}
this._toggleFavorite = this._toggleFavorite.bind(this)
this._shareFilm = this._shareFilm.bind(this)
}
_updateNavigationParams() {
this.props.navigation.setParams({
shareFilm: this._shareFilm,
film: this.state.film
})
}
componentDidMount() {
const favoriteFilmIndex = this.props.favoritesFilm.findIndex(item => item.id === this.props.navigation.state.params.idFilm)
if (favoriteFilmIndex !== -1) {
this.setState({
film: this.props.favoritesFilm[favoriteFilmIndex]
}, () => { this._updateNavigationParams() })
return
}
this.setState({ isLoading: true })
getFilmDetailFromApi(this.props.navigation.state.params.idFilm).then(data => {
this.setState({
film: data,
isLoading: false
}, () => { this._updateNavigationParams() })
})
}
_displayLoading() {
if (this.state.isLoading) {
return (
<View style={styles.loading_container}>
<ActivityIndicator size='large' />
</View>
)
}
}
_toggleFavorite() {
const action = { type: "TOGGLE_FAVORITE", value: this.state.film }
this.props.dispatch(action)
}
_displayFavoriteImage() {
var sourceImage = require('../Images/ic_favorite_border.png')
var shouldEnlarge = false // Par défaut, si le film n'est pas en favoris, on veut qu'au clic sur le bouton, celui-ci s'agrandisse => shouldEnlarge à true
if (this.props.favoritesFilm.findIndex(item => item.id === this.state.film.id) !== -1) {
sourceImage = require('../Images/ic_favorite.png')
shouldEnlarge = true // Si le film est dans les favoris, on veut qu'au clic sur le bouton, celui-ci se rétrécisse => shouldEnlarge à false
}
return (
<EnlargeShrink
shouldEnlarge={shouldEnlarge}>
<Image
style={styles.favorite_image}
source={sourceImage}
/>
</EnlargeShrink>
)
}
_displayFilm() {
const { film } = this.state
if (film != undefined) {
return (
<ScrollView style={styles.scrollview_container}>
<Image
style={styles.image}
source={{uri: getImageFromApi(film.backdrop_path)}}
/>
<Text style={styles.title_text}>{film.title}</Text>
<TouchableOpacity
style={styles.favorite_container}
onPress={() => this._toggleFavorite()}>
{this._displayFavoriteImage()}
</TouchableOpacity>
<Text style={styles.description_text}>{film.overview}</Text>
<Text style={styles.default_text}>Sorti le {moment(new Date(film.release_date)).format('DD/MM/YYYY')}</Text>
<Text style={styles.default_text}>Note : {film.vote_average} / 10</Text>
<Text style={styles.default_text}>Nombre de votes : {film.vote_count}</Text>
<Text style={styles.default_text}>Budget : {numeral(film.budget).format('0,0[.]00 $')}</Text>
<Text style={styles.default_text}>Genre(s) : {film.genres.map(function(genre){
return genre.name;
}).join(" / ")}
</Text>
<Text style={styles.default_text}>Companie(s) : {film.production_companies.map(function(company){
return company.name;
}).join(" / ")}
</Text>
</ScrollView>
)
}
}
_shareFilm() {
const { film } = this.state
Share.share({ title: film.title, message: film.overview })
}
_displayFloatingActionButton() {
const { film } = this.state
if (film != undefined && Platform.OS === 'android') {
return (
<TouchableOpacity
style={styles.share_touchable_floatingactionbutton}
onPress={() => this._shareFilm()}>
<Image
style={styles.share_image}
source={require('../Images/ic_share.png')} />
</TouchableOpacity>
)
}
}
render() {
return (
<View style={styles.main_container}>
{this._displayLoading()}
{this._displayFilm()}
{this._displayFloatingActionButton()}
</View>
)
}
}
const styles = StyleSheet.create({
main_container: {
flex: 1
},
loading_container: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center'
},
scrollview_container: {
flex: 1
},
image: {
height: 169,
margin: 5
},
title_text: {
fontWeight: 'bold',
fontSize: 35,
flex: 1,
flexWrap: 'wrap',
marginLeft: 5,
marginRight: 5,
marginTop: 10,
marginBottom: 10,
color: '#000000',
textAlign: 'center'
},
favorite_container: {
alignItems: 'center',
},
description_text: {
fontStyle: 'italic',
color: '#666666',
margin: 5,
marginBottom: 15
},
default_text: {
marginLeft: 5,
marginRight: 5,
marginTop: 5,
},
favorite_image:{
flex: 1,
width: null,
height: null
},
share_touchable_floatingactionbutton: {
position: 'absolute',
width: 60,
height: 60,
right: 30,
bottom: 30,
borderRadius: 30,
backgroundColor: '#e91e63',
justifyContent: 'center',
alignItems: 'center'
},
share_touchable_headerrightbutton: {
marginRight: 8
},
share_image: {
width: 30,
height: 30
}
})
const mapStateToProps = (state) => {
return {
favoritesFilm: state.favoritesFilm
}
}
export default connect(mapStateToProps)(FilmDetail)

Alors, vous vous en êtes sorti ? :) Ce n'était pas facile, je vous l'accorde. Dans tous les cas, du moment que vous avez essayé, le pari est gagné. Vous avez dû vous poser les bonnes questions et c'est le plus important en développement. La difficulté ici était de lancer l'animation dans le bon cycle de vie et dans la bonne fonction (  componentDidUpdate()  ).

LayoutAnimation

J'ouvre une très courte parenthèse sur l'API LayoutAnimation de React Native. Elle permet de réaliser des animations simples sur des components et sur leur layout, c'est-à-dire, leur représentation graphique. On peut donc facilement gérer des déplacements, agrandissements, etc. En fait, on aurait pu réaliser toutes les animations de ce cours avec cette API. 

Pourquoi ne l'a-t-on pas fait, alors ? Surtout si c'est plus simple ?

Je vais y venir. :) L'API LayoutAnimation ne fonctionne pas de la même manière que l'API Animated :

  • Avec l'API Animated, on définit une Animated.Value, puis on lance notre animation  start()  en lui spécifiant le type d'animation, la valeur de départ et la valeur d'arrivée

  • Avec l'API LayoutAnimation, on définit une ou plusieurs valeurs dans le state, obligatoirement, mais ce ne sont que de simples valeurs, pas d'Animated.Value. On crée ensuite notre animation, mais attention, on ne la lance pas. On modifie les valeurs de notre state avec  setState, ce qui provoque un update et un re-rendu de notre component. Et c'est au moment de se re-rendre que l'animation est jouée. C'est tordu, n'est-ce pas ? :)

Je ne suis pas spécialement fan de cette solution. Elle est certes fonctionnelle, mais on n'a pas vraiment la main sur l'exécution de l'animation et dans certains cas, c'est bloquant.

Prenons notre dernier exemple. Celui où on agrandit ou rétrécit le 🖤. Actuellement, on lance l'animation quand notre component est mis à jour (update) dans la fonction  componentDidUpdate()  . Si on veut appliquer une LayoutAnimation ici, on va devoir appeler  setState  dans la fonction  componentDidUpdate()  , vous êtes d'accord ? L'appel à  setState  va faire passer votre component dans le cycle updating, donc de nouveau dans la fonction componentDidUpdate()  , qui va appeler setState  et... vous voyez où je veux en venir ? C'est la boucle infinie, terminé, plus d'application. 

Pour vous montrer un exemple de sa simplicité, voici à quoi peut ressembler une LayoutAnimation :

LayoutAnimation.spring()
this.setState({ viewSize: 80 })

C'est tout, vraiment simple ! Si cela vous intéresse, vous trouverez plus d'informations sur la documentation officielle de React Native. Quant à moi, je vous laisse tranquille avec mes animations. :)

Nous voilà à la fin de ce chapitre consacré aux animations. J'espère qu'il vous a plu et qu'il vous a donné plein d'idées pour de futures réalisations.

On a découvert l'API Animated, une API très puissante qui vous permettra de réaliser toutes vos animations. Finalement, la mise en place des animations n'est pas très compliquée. Elle peut être résumée par la définition d'une valeur de départ, d'une valeur d'arrivée et d'un chemin, fait de montées, descentes et virages pour lier ces deux valeurs.

Même si les animations sont très enrichissantes pour une application, il ne faut pas en abuser. Elles restent des processus lourds qui peuvent, s'il y en a trop ou si elles sont trop complexes, ralentir les performances de votre application.

C'est là que je veux en venir. Parfois, vos animations vont être saccadées, lentes, pendant vos phases de développement (mode debug) et vous allez vous dire :

"OK, là je suis allé trop loin sur l'animation, il faut refaire."

Alors que pas forcément ! Votre animation peut être parfaitement fluide une fois en mode release. C'est un piège dans lequel je suis tombé, des amis aussi et peut-être vous. Dans ce cas, avant de supprimer ou de refaire votre animation qui saccade ou est ralentie, il faut la tester en mode release. Je vous montrerai comment le faire dans le tout dernier chapitre du cours.

Dans le prochain chapitre, on va utiliser les composants du device et tout ce que cela implique pour notre application. Nous verrons comment prendre une photo avec la caméra et comment récupérer les photos de la galerie de l'utilisateur. J'ai mis du temps à aborder ce chapitre sur les composants du device, mais vous allez comprendre pourquoi. :p

Exemple de certificat de réussite
Exemple de certificat de réussite