概要
CAShapeLayerで描画されたViewをアニメーションさせたいケースがあると思います。
strokeStart
やstrokeEnd
などのCAShapeLayerで定義しているプロパティを指定することで、プロパティ変化に応じてCABasicAnimationによるアニメーションを実行することができます。
しかし、描画する内容によっては、独自のプロパティを指定してアニメーションを実行させたい時があります。
そこで本記事では、円グラフを描画するCAShapeLayerクラスを例に、Animatableなプロパティを定義する方法について説明します。
完成イメージ
手順
- カスタムCAShapeLayerクラスを作成
- カスタムCAShapeLayerを持つViewを作成
- Viewを利用
カスタムCAShapeLayerクラスを作成
final class ArcLayer: CAShapeLayer {
var tintColor: UIColor = .black
@objc dynamic var startAngle: CGFloat = 0.0
@objc dynamic var endAngle: CGFloat = 0.0
override init() {
super.init()
}
override init(layer: Any) {
if let arcLayer = layer as? ArcLayer {
self.tintColor = arcLayer.tintColor
self.startAngle = arcLayer.startAngle
self.endAngle = arcLayer.endAngle
}
super.init(layer: layer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(in ctx: CGContext) {
ctx.clear(bounds)
let center = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
let radius: CGFloat = 100
let offset: CGFloat = -90
let startAngleRadians: CGFloat = (startAngle + offset).radian
let endAngleRadians: CGFloat = (endAngle + offset).radian
ctx.beginPath()
let path = CGMutablePath()
path.move(to: center)
path.addArc(
center: center,
radius: radius,
startAngle: startAngleRadians,
endAngle: endAngleRadians,
clockwise: false
)
ctx.addPath(path)
ctx.setFillColor(self.tintColor.cgColor)
ctx.setStrokeColor(UIColor.clear.cgColor)
ctx.drawPath(using: .fill)
}
override class func needsDisplay(forKey key: String) -> Bool {
if key == #keyPath(ArcLayer.endAngle) || key == #keyPath(ArcLayer.startAngle) {
return true
}
return super.needsDisplay(forKey: key)
}
}
カスタムCAShapeLayerを持つViewを作成
final class ArcView: UIView {
override class var layerClass: AnyClass {
ArcLayer.self
}
private var arcLayer: ArcLayer {
layer as? ArcLayer ?? .init()
}
var endAngle: CGFloat {
get {
arcLayer.endAngle
}
set {
arcLayer.endAngle = newValue
arcLayer.setNeedsDisplay()
}
}
override func tintColorDidChange() {
super.tintColorDidChange()
arcLayer.tintColor = tintColor
arcLayer.setNeedsDisplay()
}
func increment(newEndAngle: CGFloat) {
guard arcLayer.endAngle < 360 else { return }
arcLayer.removeAllAnimations()
let anim = CABasicAnimation(keyPath: #keyPath(ArcLayer.endAngle))
anim.fromValue = endAngle
anim.toValue = endAngle + newEndAngle
anim.duration = 0.5
endAngle = endAngle + newEndAngle
arcLayer.add(anim, forKey: "increment")
}
func decrement(newEndAngle: CGFloat) {
guard arcLayer.endAngle > 0.0 else { return }
arcLayer.removeAllAnimations()
let anim = CABasicAnimation(keyPath: #keyPath(ArcLayer.endAngle))
anim.fromValue = endAngle
anim.toValue = endAngle - newEndAngle
anim.duration = 0.5
endAngle = max(endAngle - newEndAngle, 0)
arcLayer.add(anim, forKey: "decrement")
}
}
Viewの利用
import UIKit
final class CustomCALayerProperty: UIViewController {
private var arcView = ArcView()
override func viewDidLoad() {
super.viewDidLoad()
// 省略
}
private func setupPlusMinusButtons() {
// ボタンの設定(省略)
let plusButton = UIButton()
let minusButton = UIButton()
plusButton.addAction(.init(handler: { [weak self] _ in
self?.arcView.increment(newEndAngle: 90)
}), for: .touchUpInside)
minusButton.addAction(.init(handler: { [weak self] _ in
self?.arcView.decrement(newEndAngle: 90)
}), for: .touchUpInside)
// 省略
}
}
以上です。