带有动画路径内容的 CALayer,制作 Inspectable/Designable
CALayer with Animated Path Content, Make Inspectable/Designable
我通过子类化 UIView
、添加 CAShapeLayer
子层到它的图层并覆盖 drawRect()
来更新形状图层的 path
属性.
通过制作视图 @IBDesignable
和 progress
属性 @IBInspectable
,我能够在 Interface Builder 中编辑它的值并实际看到更新后的贝塞尔曲线路径时间。非必要,但真的很酷!
接下来,我决定使路径动画化:每当您在代码中设置新值时,指示进度的圆弧应该 "grow" 从零长度到达到圆的任何百分比(想想圆弧在 Apple Watch 的 Activity 应用程序中)。
为了实现这一点,我将我的 CAShapeLayer
子层替换为自定义 CALayer
子类,该子类具有 @dynamic
(@NSManaged
) 属性,观察为动画键(我实现了 needsDisplayForKey()
、actionForKey()
、drawInContext()
等)。
我的 View 代码(相关部分)看起来像这样:
// Triggers path update (animated)
private var progress: CGFloat = 0.0 {
didSet {
updateArcLayer()
}
}
// Programmatic interface:
// (pass false to achieve immediate change)
func setValue(newValue: CGFloat, animated: Bool) {
if animated {
self.progress = newValue
} else {
arcLayer.animates = false
arcLayer.removeAllAnimations()
self.progress = newValue
arcLayer.animates = true
}
}
// Exposed to Interface Builder's inspector:
@IBInspectable var currentValue: CGFloat {
set(newValue) {
setValue(newValue: currentValue, animated: false)
self.setNeedsLayout()
}
get {
return progress
}
}
private func updateArcLayer() {
arcLayer.frame = self.layer.bounds
arcLayer.progress = progress
}
和层代码:
var animates: Bool = true
@NSManaged var progress: CGFloat
override class func needsDisplay(forKey key: String) -> Bool {
if key == "progress" {
return true
}
return super.needsDisplay(forKey: key)
}
override func action(forKey event: String) -> CAAction? {
if event == "progress" && animates == true {
return makeAnimation(forKey: event)
}
return super.action(forKey: event)
}
override func draw(in ctx: CGContext) {
ctx.beginPath()
// Define the arcs...
ctx.closePath()
ctx.setFillColor(fillColor.cgColor)
ctx.drawPath(using: CGPathDrawingMode.fill)
}
private func makeAnimation(forKey event: String) -> CABasicAnimation? {
let animation = CABasicAnimation(keyPath: event)
if let presentationLayer = self.presentation() {
animation.fromValue = presentationLayer.value(forKey: event)
}
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animation.duration = animationDuration
return animation
}
动画有效,但现在我无法在 Interface Builder 中显示我的路径。
我试过像这样实现我的观点 prepareForInterfaceBuilder()
:
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
self.topLabel.text = "Hello, Interface Builder!"
updateArcLayer()
}
...标签文本更改反映在 Interface Builder 中,但路径未呈现。
我是不是漏掉了什么?
好吧,这不是很有趣吗...原来我的 @Inspectable
属性 的声明有一个 非常愚蠢的 错误。
你能发现吗?
@IBInspectable var currentValue: CGFloat {
set(newValue) {
setValue(newValue: currentValue, animated: false)
self.setNeedsLayout()
}
get {
return progress
}
}
应该是:
@IBInspectable var currentValue: CGFloat {
set(newValue) {
setValue(newValue: newValue, animated: false)
self.setNeedsLayout()
}
get {
return progress
}
}
也就是说,我正在丢弃传递的值 (newValue
) 并使用当前值 (currentValue
) 来设置内部变量。 "Logging" Interface Builder 的值(始终为 0.0)(通过我的标签 text
属性!)给了我一个线索。
现在工作正常,不需要drawRect()
等
我通过子类化 UIView
、添加 CAShapeLayer
子层到它的图层并覆盖 drawRect()
来更新形状图层的 path
属性.
通过制作视图 @IBDesignable
和 progress
属性 @IBInspectable
,我能够在 Interface Builder 中编辑它的值并实际看到更新后的贝塞尔曲线路径时间。非必要,但真的很酷!
接下来,我决定使路径动画化:每当您在代码中设置新值时,指示进度的圆弧应该 "grow" 从零长度到达到圆的任何百分比(想想圆弧在 Apple Watch 的 Activity 应用程序中)。
为了实现这一点,我将我的 CAShapeLayer
子层替换为自定义 CALayer
子类,该子类具有 @dynamic
(@NSManaged
) 属性,观察为动画键(我实现了 needsDisplayForKey()
、actionForKey()
、drawInContext()
等)。
我的 View 代码(相关部分)看起来像这样:
// Triggers path update (animated)
private var progress: CGFloat = 0.0 {
didSet {
updateArcLayer()
}
}
// Programmatic interface:
// (pass false to achieve immediate change)
func setValue(newValue: CGFloat, animated: Bool) {
if animated {
self.progress = newValue
} else {
arcLayer.animates = false
arcLayer.removeAllAnimations()
self.progress = newValue
arcLayer.animates = true
}
}
// Exposed to Interface Builder's inspector:
@IBInspectable var currentValue: CGFloat {
set(newValue) {
setValue(newValue: currentValue, animated: false)
self.setNeedsLayout()
}
get {
return progress
}
}
private func updateArcLayer() {
arcLayer.frame = self.layer.bounds
arcLayer.progress = progress
}
和层代码:
var animates: Bool = true
@NSManaged var progress: CGFloat
override class func needsDisplay(forKey key: String) -> Bool {
if key == "progress" {
return true
}
return super.needsDisplay(forKey: key)
}
override func action(forKey event: String) -> CAAction? {
if event == "progress" && animates == true {
return makeAnimation(forKey: event)
}
return super.action(forKey: event)
}
override func draw(in ctx: CGContext) {
ctx.beginPath()
// Define the arcs...
ctx.closePath()
ctx.setFillColor(fillColor.cgColor)
ctx.drawPath(using: CGPathDrawingMode.fill)
}
private func makeAnimation(forKey event: String) -> CABasicAnimation? {
let animation = CABasicAnimation(keyPath: event)
if let presentationLayer = self.presentation() {
animation.fromValue = presentationLayer.value(forKey: event)
}
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animation.duration = animationDuration
return animation
}
动画有效,但现在我无法在 Interface Builder 中显示我的路径。
我试过像这样实现我的观点 prepareForInterfaceBuilder()
:
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
self.topLabel.text = "Hello, Interface Builder!"
updateArcLayer()
}
...标签文本更改反映在 Interface Builder 中,但路径未呈现。
我是不是漏掉了什么?
好吧,这不是很有趣吗...原来我的 @Inspectable
属性 的声明有一个 非常愚蠢的 错误。
你能发现吗?
@IBInspectable var currentValue: CGFloat {
set(newValue) {
setValue(newValue: currentValue, animated: false)
self.setNeedsLayout()
}
get {
return progress
}
}
应该是:
@IBInspectable var currentValue: CGFloat {
set(newValue) {
setValue(newValue: newValue, animated: false)
self.setNeedsLayout()
}
get {
return progress
}
}
也就是说,我正在丢弃传递的值 (newValue
) 并使用当前值 (currentValue
) 来设置内部变量。 "Logging" Interface Builder 的值(始终为 0.0)(通过我的标签 text
属性!)给了我一个线索。
现在工作正常,不需要drawRect()
等