Pourquoi et comment faire des animations sur iOS - CoreAnimation

le 29/09/2017 par Renaud Cousin
Tags: Software Engineering

Considérées par certains comme optionnelles, les animations dans les applications mobiles sont pourtant très importantes et peuvent grandement améliorer l’expérience utilisateur. Au travers d’une séries d’articles consacrés aux animations sur iOS, je souhaite donc explorer les possibilités offertes par le framework CoreAnimation mis à disposition par Apple. Je commencerai dans ce premier article par une introduction aux concepts d’animation, et comment ils se traduisent sur la plateforme d’Apple. D’autres articles viendront par la suite expliquer pas-à-pas les différents types d’animations.

I. Importances des animations

En tant qu'ancien développeur Flash/ActionScript, j’ai été amené à faire de l’animation quotidiennement puisque le brief initial pour chaque site Web à l’époque était :

Tout doit être animé ! Les roll over/out/click de bouton, l’apparition/disparition des pages ainsi que le resize !

Aussi, lorsque je me suis reconverti au développement iOS, j’ai été très surpris de voir que très peu de développeurs intégraient des animations dans leurs développements — se contentant de n’utiliser que des composants natifs tels quels. Je ne parle pas ici de transformer une application en sapin de Noël en ajoutant des animations partout, mais je suis intimement convaincu que quelques animations discrètes et bien placées peuvent changer le regard des utilisateurs sur nos applications.

D’ailleurs, si on regarde plus attentivement les applications que nous utilisons tous (parfois même quotidiennement), on s’aperçoit que beaucoup d’entre elles intègrent ces animations, sans que l’on y prête attention. Et pourtant, observez ces mêmes applications sans animations et voyez la différence de perception que nous en avons :

Facebook

    

Twitter

    

LinkedIn

    

AppleMusic

    

II. Généralités

Cette vie passée de développeur ActionScript m’a permis d’acquérir les bases de l’animation et une certaine sensibilité du rythme nécessaire pour faire de jolies animations — le rythme de l’animation ainsi que le séquençage sont extrêmement importants pour obtenir un effet visuel qui attire l’oeil.

Parmi les notions importantes à acquérir, il y a la construction, c’est-à-dire apprendre à réfléchir à la hiérarchie des objets entre eux pour obtenir une animation de qualité, mais surtout pour conserver un code lisible et clair, ainsi qu'une optimisation d'exécution. En effet, il est nécessaire de regrouper — dans la mesure du possible — les objets ayant des animations communes dans des containers, plutôt que d’appliquer des animations identiques à plusieurs objets en parallèle.

Par exemple, pour une apparition simple de plusieurs éléments avec une opacité et une translation identique :

Les 3 objets sont inclus dans un container, et c’est ce container auquel nous allons appliquer une animation :

Idem pour ce type d’animation (nous avons juste déplacé l’anchorPoint du layer du container pour avoir un centre de rotation différent) :

Ça semble logique, mais ce n’est pas forcément évident dans certains cas plus complexes.

En bref, ce qu’il faut retenir ici et avant tout, c’est que l’animation ne s’improvise pas à la fin du développement ! La hiérarchie des objets étant dépendante des animations voulues, il est nécessaire d'avoir une réflexion en amont du développement, sans quoi vous devrez revoir l'organisation de votre code plusieurs fois. Si vous ne prenez pas le temps nécessaire pour bien penser votre animation, vous allez vous retrouver avec un code beaucoup plus complexe et obtenir un rendu de moindre qualité.

III. Théorie de l'animation sur iOS

Sur iOS, il est tout à fait possible de faire des animations directement au niveau UIView, en utilisant une des méthodes statiques :class func animate(withDuration duration: TimeInterval,                                                       delay: TimeInterval,                                                    options: UIViewAnimationOptions = [],                                              animations: @escaping () -> Void,                                              completion: ((Bool) -> Void)? = nil)

On peut également passer par le récent UIViewPropertyAnimator d’iOS 10.

Cependant, nous allons plutôt nous attacher ici à CoreAnimation pour les possibilités qu'il nous offre en terme de complexité d’animation.

CoreAnimation

CoreAnimation est un framework d’Apple offrant tous les outils nécessaires à l’animation de vos vues, tout en conservant d’excellentes performances d’affichage.

CALayer

La classe de base de CoreAnimation, qui sera au coeur de toutes vos animations est CALayer — et toutes ses sous-classes.

L’objet CALayer, présent dans tout UIView via la propriété layer, sert à gérer le contenu graphique de la vue. Il est extrêment puissant de part ses nombreuses propriétés permettant de gérer aussi bien une ombre qu’un masque, mais je ne m’étendrai pas sur l’usage de CALayer dans cet article. Pour plus d'informations, vous trouverez ici toutes les propriétés animables des layers.

De plus, CALayer tout comme UIView, peut servir de container : vous pouvez ajouter, supprimer, remplacer des sous-layers à vos layers.

Enfin, CALayer est la classe de base, mais vous pouvez utiliser une de ses sous-classes pour des comportements plus spécifiques (ou sous-classer vous-même CALayer).

On retrouve ainsi en sous-classes de CALayer :

  • CAEmitterLayer permet de créer un système de particules
  • CAGradientLayer est utile pour les dégradés
  • CAReplicatorLayer à utiliser pour dupliquer un objet (layer) animé avec des offsets (couleur, position, délais d’animation) — Exemples pour mieux comprendre
  • CAShapeLayer est utile pour travailler à partir de path (courbes de bezier, …)
  • CATextLayer est utilisé pour l’affichage de texte
  • CATransformLayer permet de faire de l’affichage 3D puisque les sous-layers ne sont pas aplatis dans ce type de layer
  • CATiledLayer à utiliser pour l’affichage de très grandes images subdivisées en plus petites images affichées individuellement (comme GoogleMaps), supportant le zoom
  • ainsi que quelques autres encore plus spécifiques (dont vous ne devriez pas avoir besoin dans un premier temps) listés ici.

Pour utiliser ces sous-classes de CALayer, deux moyens à votre disposition :

  • Soit vous créez une instance de CAGradientLayer par exemple, et vous l’ajoutez à votre layer :var gradientLayer = CAGradientLayer() layer.addSublayer(gradientLayer)

  • Soit vous surchargez la variable layerClass de votre custom UIView :override class var layerClass: AnyClass {     return CAGradientLayer.self }

CAAnimation

CAAnimation — et surtout toutes ses sous-classes puisque c’est une classe abstraite — est la classe indispensable pour faire vos animations.

CAAnimation expose les propriétés :

  • timingFunction qui correspond au type d’interpolation de l’animation (linéaire par défaut) : est-ce que l’animation doit accélérer puis ralentir, doit-il y avoir un effet rebond ? — Pour réaliser vos propres CAMediaTimingFunction, ce site vous propose de voir directement le rendu de votre déplacement (sans avoir à compiler 50x le projet Xcode pour tester, parce que trouver les bonnes valeurs pour une animation fluide peut être très chronophage !)
  • isRemovedOnCompletion : CAAnimation effectue une copie du layer pour l’animation, il n’y a pas de modification du layer de départ. Si isRemovedOnCompletion est à false — true par défaut — et que vous avez des interactions avec votre layer, celles-ci ne fonctionneront pas. À l’inverse, lorsque l’animation va se terminer, la copie du layer va disparaître et le layer va s’afficher de nouveau dans son état initial (voir fillMode du protocol CAMediaTiming adopté par CAAnimation)
  • delegate qui permet d’être informé lorsque l’animation commence ou se termine
  • et toutes les propriétés du protocol CAMediaTiming qui permettent un contrôle sur la répétition, vitesse ou encore offset de temps de l’animation — et le fillMode mentionné plus haut.

Enfin, pour créer une animation sur le layer (et la lancer), il suffit d’appeler la méthode de CALayer :func add(_ anim: CAAnimation, forKey key: String?)

Connaissant désormais les bases de CAAnimation, voyons les sous-classes qui nous sont proposées, et dans quels cas elle vous seront utiles :

  • CATransaction permet de faire des transitions entre différents états d’un layer avec des transitions de type reveal, push, move (cf. gif suivant) avec un sous-type déterminant la direction — je ne le détaillerais pas particulièrement car il n’est à mon sens utile que dans de très rares cas.let transitioningLayer = CATextLayer() ... view.layer.addSublayer(transitioningLayer) transitioningLayer.backgroundColor = UIColor.red.cgColor transitioningLayer.string = “Red” ... let transition = CATransition() transition.duration = 0.5     transition.type = kCATransitionPush transition.subtype = kCATransitionFromLeft     transitioningLayer.add(transition,                       forKey: “transition”) transitioningLayer.backgroundColor = UIColor.blue.cgColor transitioningLayer.string = "Blue"

  • CABasicAnimation qui permet de faire une animation toute simple sur une propriété d’un layerlet animation = CABasicAnimation(keyPath: “cornerRadius”) animation.duration = 1.0 animation.fromValue = 0 animation.toValue = view.frame.width * 0.5 view.layer.add(animation, forKey: “cornerRadiusAnimation”)

  • CAKeyFrameAnimation permet un contrôle plus pointu sur l’animation puisque vous définissez les valeurs de l’animation par keyframe — c'est-à-dire les différentes valeurs clés de l'animation — avec des timingFunctions pour chaque portion de l'animation (à la différence de CABasicAnimation où l'on ne peut passer qu'une seule TimingFunction pour toute l'animation).let animation = CAKeyframeAnimation(keyPath: “bounds.size.width”) animation.keyTimes = [0, 1] animation.values = [30, 200] animation.duration = 0.6 animation.timingFunctions = [CAMediaTimingFunction(controlPoints: 0.5, 0, 0.4, 1.45)] animation.isRemovedOnCompletion = false animation.fillMode = kCAFillModeForwards view.layer.add(animation, forKey: “widthAnimation”) Vous pouvez également animer le long d’un ****path****let path = CGPath(ellipseIn: CGRect(x: 0, y: 0, width: 128, height: 128), transform: nil) let anim = CAKeyframeAnimation(keyPath: “position”) anim.duration = 2.0 anim.path = path anim.isRemovedOnCompletion = false anim.fillMode = kCAFillModeForwards anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) layer.add(anim, forKey: “positionAlongPath”)

  • CAAnimationGroup, comme son nom l’indique, permet des grouper des animations via la propriété animations: [CAAnimation]?. C’est à ce moment que vous allez pouvoir profiter pleinement de CAKeyframeAnimation, pour synchroniser vos animations ensemble !

Nous aurons l’occasion de voir un exemple détaillé de ce type d’animation dans le prochain article.

IV. Tips

Maintenant que nous avons une bonne vision d’ensemble des mécaniques d’animation sur iOS, je souhaite vous donner quelques billes sur les écueils à éviter lors de vos futurs développements.

  • isRemovedOnCompletion et fillMode

Comme je le disais précédemment, les animations ne modifient absolument pas le layer auquel elles sont appliquées : elles effectuent une copie du layer et animent ensuite cette copie. De ce fait, il y a deux propriétés de CAAnimation qui vous seront indispensables : fillMode et isRemovedOnCompletion. Par défaut, lorsque vous créez une animation, isRemovedOnCompletion est à true et fillMode est à kCAFillModeRemoved. C’est-à-dire qu’à la fin de votre animation, la copie animée du layer va être supprimée (isRemovedOnCompletion = true), donc votre layer va s’afficher dans son état initial. Comme ceci :

Pour éviter cet effet, vous pouvez passer isRemovedOnCompletion = false et fillMode = kCAFillModeFowards. Cette dernière valeur de fillMode indique que l’animation reste dans son état final — vous trouverez le détail des autres valeurs possibles ici.

Cependant, si vous avez prévu des interactions avec votre layer, celles-ci ne fonctionneront plus puisqu’il y aura un objet (la copie de votre layer) devant le layer.

Dans ce cas, il est nécessaire de laisser la valeur isRemovedOnCompletion = true, vous n’avez plus à vous préoccuper de la valeur de fillMode, par contre vous devez modifier votre layer une fois que vous aurez ajouté son animation :let animation = CAKeyframeAnimation(keyPath: “bounds.size.width”) animation.keyTimes = [0, 1] animation.values = [30, 200] animation.duration = 0.6 animation.timingFunctions = [CAMediaTimingFunction(controlPoints: 0.5, 0, 0.4, 1.45)] animation.isRemovedOnCompletion = false animation.fillMode = kCAFillModeForwards view.layer.add(animation, forKey: “widthAnimation”) // --> modify layer to fit animation end state view.layer.bounds = CGRect(x: 0, y: 0, with: 200, height: view.layer.bounds.height)

  • beginTime et fillMode

Très vite, lors de la création de vos animations, vous allez vous dire :

“Mais … Comment j’ajoute un délais avant le démarrage de mon animation ??“

C’est là qu’intervient la propriété beginTime du protocol CAMediaTiming. Cette propriété permet effectivement d’ajouter un délais et fonctionne en conjonction avec la méthode statique CACurrentMediaTiming(). Cependant, en ajoutant un délais à votre animation, vous allez devoir changer la valeur de la propriété fillMode — vue précédemment — pour lui donner la valeur kCAFillModeBackwards, sans quoi vous allez voir l’état final de votre animation avant que l’animation ne se lance. Ceci est dû au fait que nous modifions notre layer pour lui donner l’apparence de fin d’animation après lui avoir ajouté l’animation comme vu juste avant.let animation = CAKeyframeAnimation(keyPath: “bounds.size.width”) [...] animation.beginTime = CACurrentMediaTiming() + 1 animation.fillMode = kCAFillModeBackwards view.layer.add(animation, forKey: “widthAnimation”)

Enfin, dans le cas de CAGroupAnimation, vous allez utiliser la propriété timeOffset de chaque animation du groupe.

  • CAAnimation et AutoLayout

AutoLayout peut induire des comportements étranges dès lors que vous touchez à la position ou aux dimensions de layer d’une vue utilisant le système de contraintes d’affichage Apple. AutoLayout arrive en conflit avec les valeurs de positions et tailles de l’animation. Dans ce cas, autant utiliser la hiérarchie de layer et ne pas modifier le layer direct de la vue gérée par AutoLayout — j’entends par là qu’il vaut mieux créer des sous-layers et leur appliquer les animations — ou alors embarquer la vue (sans lui donner de contraintes) dans une vue qui servira de container (et qui elle, aura AutoLayout).

  • anchorPoint

L’anchorPoint d’un layer est en quelque sorte son centre de gravité. C’est le point de référence autour duquel seront appliquées les transformations (rotation/scale). L’anchorPoint est un CGPoint dont la valeur par défaut correspond au centre de la vue (donc la rotation se fait autour du centre de la vue). Vous trouverez ici un excellent article sur l’anchorPoint. Par contre, attention si vous êtes amenés à le modifier : anchorPoint, position et frame sont intimement liés. Ainsi, une modification de l’anchorPoint modifiera sa frame/position (et donc son affichage à l’écran).

  • CATransform3D

Contrairement à toutes les modifications (CGAffineTransform) sur UIView, CALayer gère l’affichage en 3D : la propriété transform de CALayer est CATransform3D. Il est donc aisé de faire des rotations 3D (entre autres).

  • 3D et perspective

Vous pouvez ajouter un effet de perspective à vos sous-layers en utilisant la propriété (très explicite comme vous pouvez le voir ...) m34 de CATransform3D de cette manière :var perspective = CATransform3DIdentity perspective.m34 = -1.0 / 100 layer.sublayerTransform = perspective

  • Last but not least

Les animations  s’arrêtent lorsque l’application passe en background ! À ne pas oublier dans le cas où vous avez une animation permanente — en background par exemple.

Ce n’est pas un soucis s’il s’agit d’une animation ponctuelle, cependant si c’est une animation qui doit tourner en permanence en fond, ça devient un vrai soucis. Dans ce cas, il suffit de passer la propriété isRemovedOnCompletion de votre animation à false.

To be continued ...

Lors d’un prochain article, je vous proposerai de réaliser différentes animations pas-à-pas. Des animations sans interactions, des animations avec interactions. Je vous montrerai différents de types de layers pour pouvoir vous rendre compte des possibilités.