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 ? ☺️
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)
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).
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
}
}
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
}
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/