Créer une "CALayer Animatable property" personnalisée sur iOS
Cet article faisant suite aux deux précédents de Renaud Cousin (liens en bas de page), nous continuons à explorer les animations sur iOS et nous allons voir comment animer une propriété custom d'une vue. Si on peut bien accorder une chose à iOS c'est que son framework graphique (UIKit) est très bien pensé. Pour faire varier la couleur d'une vue en l'animant, on peut tout simplement faire:
view.backgroundColor = UIColor.red
UIView.animate(withDuration: 1.0) {
view.backgroundColor = UIColor.blue
}
Résultat:
Simple et efficace. Cool pas vrai ? ☺️
Et si on créait notre propre propriété
Bien qu'UIKit
nous donne un certain nombre de propriété "animable": backgroundColor
, frame
, opacity
, position
pour les plus connues.
Il serait intéressant de pouvoir en créer une nous même. Quelque chose dans ce genre là:
UIView.animate(withDuration: 1.0) {
view.distortion = 1.0
}
Où la distortion
serait une propriété qui déforme notre UIView
et dont la valeur varie de 0.0
à 1.0
.
À noter que cette propriété serait pleinement compatible avec le framework UIKit
, on pourrait donc s'amuser à faire des variations de la sorte sans problème.
UIView.animate(
withDuration: 1.0,
delay: 0.0,
usingSpringWithDamping: 0.4,
initialSpringVelocity: 0.6,
options: [],
animations: {
view.distortion = 1.0
}, completion: nil)
1. Commençons
Comme vous le savez une UIView
s’occupe de la mise en page, de la gestion des événements tactiles. En revanche elle ne s’occupe pas directement du dessin ou des animations. UIKit
délègue cette tâche à CoreAnimation
. UIView
est en fait juste un wrapper sur CALayer
. Lorsque vous définissez une taille sur votre UIView, la vue définit simplement la taille sur sa couche de support CALayer
. Si vous appelez layoutIfNeeded
sur une UIView
, l’appel est transféré à la couche racine CALayer
. Chaque UIView
a une couche CALayer
racine, qui peut contenir des sous-couches.
Enfin un CALayer
possède une propriété presentationLayer
qui retourne une copie de lui même représentant l'état de la couche qui est actuellement affichée à l'écran (utile lors d'une animation par exemple).
2. Définissez votre "layer"
Nous allons donc commencer par créer notre propre Layer.
class DistortionLayer: CAShapeLayer {
@NSManaged var distortion: CGFloat
override init() {
super.init()
}
override init(layer: Any) {
super.init(layer: layer)
if let layer = layer as? DistortionLayer {
distortion = layer.distortion
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private class func isCustomAnimKey(_ key: String) -> Bool {
return key == "distortion"
}
override class func needsDisplay(forKey key: String) -> Bool {
if self.isCustomAnimKey(key) {
return true
}
return super.needsDisplay(forKey: key)
}
override func action(forKey event: String) -> CAAction? {
guard DistortionLayer.isCustomAnimKey(event) else { return super.action(forKey: event) }
guard let action = super.action(forKey: #keyPath(backgroundColor)) as? CAAnimation,
let animation: CABasicAnimation = (action.copy() as? CABasicAnimation) else {
setNeedsDisplay()
return nil
}
if let presentationLayer = presentation() {
animation.fromValue = presentationLayer.distortion
}
animation.keyPath = event
animation.toValue = nil
return animation
}
}
- Notez la présence du @NSManaged qui permet de prévenir le compilateur que cette propriété sera initialisé au runtime.
- Dans la méthode action nous nous basons sur l'animation de la propriété backgroundColor et en faisons une copie, libre à vous de définir une CABasicAnimation ou de copier l'animation d'une autre propriété déjà existante. (Nota bene: vous n'êtes pas obligé d'animer la propriété backgroundColor pour que l'animation distortion soit effective).
3. Définissez votre vue
class DistortionView: UIView {
var distortion: CGFloat {
set {
(layer as? DistortionLayer)?.distortion = newValue
}
get {
return (layer as? DistortionLayer)?.distortion ?? 0
}
}
override class var layerClass: AnyClass {
return DistortionLayer.self
}
override func display(_ layer: CALayer) {
guard let presentationLayer = layer.presentation() as? DistortionLayer else { return }
guard let castLayer = layer as? CAShapeLayer else { return }
let width = frame.width
let height = frame.height
let distortionValue = (max(width, height)/8) * presentationLayer.distortion
let x0 = CGPoint(x: 0, y: 0)
let p0 = CGPoint(x: width/2, y: 0 + distortionValue)
let x1 = CGPoint(x: width, y: 0)
let p1 = CGPoint(x: width - distortionValue, y: height/2)
let x2 = CGPoint(x: width, y: height)
let p2 = CGPoint(x: width/2, y: height - distortionValue)
let x3 = CGPoint(x: 0, y: height)
let p3 = CGPoint(x: 0 + distortionValue, y: height/2)
let path = UIBezierPath()
path.move(to: x0)
path.addQuadCurve(to: x1, controlPoint: p0)
path.addQuadCurve(to: x2, controlPoint: p1)
path.addQuadCurve(to: x3, controlPoint: p2)
path.addQuadCurve(to: x0, controlPoint: p3)
path.close()
let maskShape = CAShapeLayer()
maskShape.path = path.cgPath
castLayer.mask = maskShape
}
- Dans la fonction display vous pouvez utiliser la propriété distortion qui varie entre 0.0 et 1.0 (durant l'animation) pour dessiner votre vue. Ici en l'occurrence on dessine un carré en faisant varier les points de Bézier afin de déformer sa structure.
- N'oubliez pas de surcharger la layerClass de votre vue par votre custom layer avec override class var layerClass: AnyClass
4. Animez votre propriété
Comme nous le remarquions plus haut, cette propriété est pleinement compatible avec les APIs UIKit. On peut donc jouer sur deux propriétés par exemple.
self.distortionView.backgroundColor = UIColor.red
self.distortionView.distortion = 0
UIView.animate(withDuration: 2.0) {
self.distortionView.backgroundColor = UIColor.blue
self.distortionView.distortion = 1
}
Résultat:
Ou bien ajouter un damping et une vélocité:
self.distortionView.backgroundColor = UIColor.red
self.distortionView.distortion = 0
UIView.animate(withDuration: 1.0,
delay: 0.0,
usingSpringWithDamping: 0.4,
initialSpringVelocity: 0.6,
options: [],
animations: {
self.distortionView.distortion = 1
self.distortionView.backgroundColor = UIColor.blue
}, completion: nil)
Résultat:
Ou ajouter une completion
self.distortionView.backgroundColor = UIColor.red
self.distortionView.distortion = 0
UIView.animate(withDuration: 0.3,
delay: 0.0,
options:[.curveEaseOut, .autoreverse],
animations: {
self.distortionView.distortion = 1
self.distortionView.backgroundColor = UIColor.blue
},completion: { finished in
if finished == true {
self.distortionView.distortion = 0
self.distortionView.backgroundColor = UIColor.red
}
})
Résultat:
Le fait que la propriété soit compatible avec les APIs UIKit nous permet de modifier très simplement l'animation de notre vue.
https://blog.octo.com/pourquoi-et-comment-faire-des-animations-sur-ios-coreanimation/
https://blog.octo.com/les-animations-sur-ios-par-la-pratique-careplicatorlayer/