Exemples pour mieux comprendre
Pour utiliser ces sous-classes de CALayer, deux moyens à votre disposition :
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 :
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.
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.
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)
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.
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).
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).
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).
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
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.
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.