如何在 iOS 中为自定义 属性 设置动画
How to animate a custom property in iOS
我有一个自定义 UIView,它使用 Core Graphics 调用绘制其内容。一切正常,但现在我想为影响显示的值变化设置动画。我有一个自定义 属性 来在我的自定义 UView 中实现此目的:
var _anime: CGFloat = 0
var anime: CGFloat {
set {
_anime = newValue
for(gauge) in gauges {
gauge.animate(newValue)
}
setNeedsDisplay()
}
get {
return _anime
}
}
我已经从 ViewController:
开始制作动画
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.emaxView.anime = 0.5
UIView.animate(withDuration: 4) {
DDLogDebug("in animations")
self.emaxView.anime = 1.0
}
}
这不起作用 - 动画值确实从 0.5 变为 1.0,但它会立即改变。对动漫 setter 有两次调用,一次调用值为 0.5,然后立即调用值为 1.0。如果我将 属性 更改为标准 UIView 属性 的动画,例如阿尔法,它工作正常。
我来自 Android 背景,所以整个 iOS 动画框架对我来说看起来像是黑魔法。除了预定义的 UIView 属性之外,还有什么方法可以为 属性 设置动画吗?
下面是动画视图的外观 - 它大约每 1/2 秒获得一个新值,我希望指针在这段时间内从前一个值平滑移动到下一个值。更新它的代码是:
open func animate(_ progress: CGFloat) {
//DDLogDebug("in animate: progress \(progress)")
if(dataValid) {
currentValue = targetValue * progress + initialValue * (1 - progress)
}
}
并且在更新后调用 draw()
将使它使用新的指针位置重绘,在 initialValue
和 targetValue
之间进行插值
如果您在 gauge.animate(newValue)
函数中修改任何自动布局约束,则需要调用 layoufIfNeeded()
而不是 setNeedsDisplay()
。
简短回答:使用 CADisplayLink
每 n 帧调用一次。示例代码:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let displayLink = CADisplayLink(target: self, selector: #selector(animationDidUpdate))
displayLink.preferredFramesPerSecond = 50
displayLink.add(to: .main, forMode: .defaultRunLoopMode)
updateValues()
}
var animationComplete = false
var lastUpdateTime = CACurrentMediaTime()
func updateValues() {
self.emaxView.animate(0);
lastUpdateTime = CACurrentMediaTime()
animationComplete = false
}
func animationDidUpdate(displayLink: CADisplayLink) {
if(!animationComplete) {
let now = CACurrentMediaTime()
let interval = (CACurrentMediaTime() - lastUpdateTime)/animationDuration
self.emaxView.animate(min(CGFloat(interval), 1))
animationComplete = interval >= 1.0
}
}
}
代码可以改进和概括,但它正在做我需要的工作。
如果完全使用 CoreGraphics 绘制,如果您想做一些数学运算,可以使用一种非常简单的方法来制作动画。幸运的是你有一个刻度可以告诉你要旋转的弧度数,所以数学是最小的并且不涉及三角函数。这样做的好处是您不必重新绘制整个背景,甚至指针。正确设置角度和东西可能有点棘手,如果以下方法不起作用,我可以提供帮助。
在draw(in rect)中正常绘制view的背景。您应该放入 CALayer 的指针。您几乎可以将指针的绘制代码(包括中心深灰色圆圈)移动到 returns UIImage 的单独方法中。该层将根据视图的框架调整大小(在布局子视图中),并且锚点必须设置为 (0.5, 0.5),这实际上是默认设置,所以您应该可以将那条线排除在外。然后你的动画方法只是改变图层的变换以根据你的需要旋转。这是我会怎么做。我将更改方法和变量名称,因为 anime 和 animate 有点太晦涩了。
因为图层属性隐式设置了持续时间为 0.25 的动画,您甚至可以在不调用动画方法的情况下逃脱。我已经有一段时间没有使用 CoreAnimation 了,所以显然要测试一下。
这里的优点是您只需将转盘的 RPM 设置为您想要的值,它就会以该速度旋转。没有人会在阅读您的代码时觉得 WTF 就是一部动漫! :) 我已经包含了 init 方法来提醒你改变图层的内容比例(或者它以低质量呈现),显然你的 init 中可能还有其他东西。
class SpeedDial: UIView {
var pointer: CALayer!
var pointerView: UIView!
var rpm: CGFloat = 0 {
didSet {
pointer.setAffineTransform(rpm == 0 ? .identity : CGAffineTransform(rotationAngle: rpm/25 * .pi))
}
}
override init(frame: CGRect) {
super.init(frame: frame)
pointer = CALayer()
pointer.contentsScale = UIScreen.main.scale
pointerView = UIView()
addSubview(pointerView)
pointerView.layer.addSublayer(pointer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
pointer = CALayer()
pointer.contentsScale = UIScreen.main.scale
pointerView = UIView()
addSubview(pointerView)
pointerView.layer.addSublayer(pointer)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
//draw background with values
//but not the pointer or centre circle
context.restoreGState()
}
override func layoutSubviews() {
super.layoutSubviews()
pointerView.frame = bounds
pointer.frame = bounds
pointer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
pointer.contents = drawPointer(in: bounds)?.cgImage
}
func drawPointer(in rect: CGRect) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
context.saveGState()
// draw the pointer Image. Make sure to draw it pointing at zero. ie at 8 o'clock
// I'm not sure what your drawing code looks like, but if the pointer is pointing
// vertically(at 12 o'clock), you can get it pointing at zero by rotating the actual draw context like so:
// perform this context rotation before actually drawing the pointer
context.translateBy(x: rect.width/2, y: rect.height/2)
context.rotate(by: -17.5/25 * .pi) // the angle judging by the dial - remember .pi is 180 degrees
context.translateBy(x: -rect.width/2, y: -rect.height/2)
context.restoreGState()
let pointerImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return pointerImage
}
}
指针的身份变换使其指向 0 RPM,因此每次将 RPM 提高到您想要的值时,它都会旋转到该值。
编辑:测试过,有效。除了我犯了几个错误——你不需要改变图层位置,我相应地更新了代码。此外,更改层的转换会触发直接父级中的 layoutSubviews。我忘了这个。解决此问题的最简单方法是将指针层放入作为 SpeedDial 子视图的 UIView。我已经更新了代码。祝你好运!也许这有点矫枉过正,但它比对视图、背景和所有内容的整个渲染进行动画处理更具可重用性。
我有一个自定义 UIView,它使用 Core Graphics 调用绘制其内容。一切正常,但现在我想为影响显示的值变化设置动画。我有一个自定义 属性 来在我的自定义 UView 中实现此目的:
var _anime: CGFloat = 0
var anime: CGFloat {
set {
_anime = newValue
for(gauge) in gauges {
gauge.animate(newValue)
}
setNeedsDisplay()
}
get {
return _anime
}
}
我已经从 ViewController:
开始制作动画override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.emaxView.anime = 0.5
UIView.animate(withDuration: 4) {
DDLogDebug("in animations")
self.emaxView.anime = 1.0
}
}
这不起作用 - 动画值确实从 0.5 变为 1.0,但它会立即改变。对动漫 setter 有两次调用,一次调用值为 0.5,然后立即调用值为 1.0。如果我将 属性 更改为标准 UIView 属性 的动画,例如阿尔法,它工作正常。
我来自 Android 背景,所以整个 iOS 动画框架对我来说看起来像是黑魔法。除了预定义的 UIView 属性之外,还有什么方法可以为 属性 设置动画吗?
下面是动画视图的外观 - 它大约每 1/2 秒获得一个新值,我希望指针在这段时间内从前一个值平滑移动到下一个值。更新它的代码是:
open func animate(_ progress: CGFloat) {
//DDLogDebug("in animate: progress \(progress)")
if(dataValid) {
currentValue = targetValue * progress + initialValue * (1 - progress)
}
}
并且在更新后调用 draw()
将使它使用新的指针位置重绘,在 initialValue
和 targetValue
如果您在 gauge.animate(newValue)
函数中修改任何自动布局约束,则需要调用 layoufIfNeeded()
而不是 setNeedsDisplay()
。
简短回答:使用 CADisplayLink
每 n 帧调用一次。示例代码:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let displayLink = CADisplayLink(target: self, selector: #selector(animationDidUpdate))
displayLink.preferredFramesPerSecond = 50
displayLink.add(to: .main, forMode: .defaultRunLoopMode)
updateValues()
}
var animationComplete = false
var lastUpdateTime = CACurrentMediaTime()
func updateValues() {
self.emaxView.animate(0);
lastUpdateTime = CACurrentMediaTime()
animationComplete = false
}
func animationDidUpdate(displayLink: CADisplayLink) {
if(!animationComplete) {
let now = CACurrentMediaTime()
let interval = (CACurrentMediaTime() - lastUpdateTime)/animationDuration
self.emaxView.animate(min(CGFloat(interval), 1))
animationComplete = interval >= 1.0
}
}
}
代码可以改进和概括,但它正在做我需要的工作。
如果完全使用 CoreGraphics 绘制,如果您想做一些数学运算,可以使用一种非常简单的方法来制作动画。幸运的是你有一个刻度可以告诉你要旋转的弧度数,所以数学是最小的并且不涉及三角函数。这样做的好处是您不必重新绘制整个背景,甚至指针。正确设置角度和东西可能有点棘手,如果以下方法不起作用,我可以提供帮助。
在draw(in rect)中正常绘制view的背景。您应该放入 CALayer 的指针。您几乎可以将指针的绘制代码(包括中心深灰色圆圈)移动到 returns UIImage 的单独方法中。该层将根据视图的框架调整大小(在布局子视图中),并且锚点必须设置为 (0.5, 0.5),这实际上是默认设置,所以您应该可以将那条线排除在外。然后你的动画方法只是改变图层的变换以根据你的需要旋转。这是我会怎么做。我将更改方法和变量名称,因为 anime 和 animate 有点太晦涩了。
因为图层属性隐式设置了持续时间为 0.25 的动画,您甚至可以在不调用动画方法的情况下逃脱。我已经有一段时间没有使用 CoreAnimation 了,所以显然要测试一下。
这里的优点是您只需将转盘的 RPM 设置为您想要的值,它就会以该速度旋转。没有人会在阅读您的代码时觉得 WTF 就是一部动漫! :) 我已经包含了 init 方法来提醒你改变图层的内容比例(或者它以低质量呈现),显然你的 init 中可能还有其他东西。
class SpeedDial: UIView {
var pointer: CALayer!
var pointerView: UIView!
var rpm: CGFloat = 0 {
didSet {
pointer.setAffineTransform(rpm == 0 ? .identity : CGAffineTransform(rotationAngle: rpm/25 * .pi))
}
}
override init(frame: CGRect) {
super.init(frame: frame)
pointer = CALayer()
pointer.contentsScale = UIScreen.main.scale
pointerView = UIView()
addSubview(pointerView)
pointerView.layer.addSublayer(pointer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
pointer = CALayer()
pointer.contentsScale = UIScreen.main.scale
pointerView = UIView()
addSubview(pointerView)
pointerView.layer.addSublayer(pointer)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
//draw background with values
//but not the pointer or centre circle
context.restoreGState()
}
override func layoutSubviews() {
super.layoutSubviews()
pointerView.frame = bounds
pointer.frame = bounds
pointer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
pointer.contents = drawPointer(in: bounds)?.cgImage
}
func drawPointer(in rect: CGRect) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
context.saveGState()
// draw the pointer Image. Make sure to draw it pointing at zero. ie at 8 o'clock
// I'm not sure what your drawing code looks like, but if the pointer is pointing
// vertically(at 12 o'clock), you can get it pointing at zero by rotating the actual draw context like so:
// perform this context rotation before actually drawing the pointer
context.translateBy(x: rect.width/2, y: rect.height/2)
context.rotate(by: -17.5/25 * .pi) // the angle judging by the dial - remember .pi is 180 degrees
context.translateBy(x: -rect.width/2, y: -rect.height/2)
context.restoreGState()
let pointerImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return pointerImage
}
}
指针的身份变换使其指向 0 RPM,因此每次将 RPM 提高到您想要的值时,它都会旋转到该值。
编辑:测试过,有效。除了我犯了几个错误——你不需要改变图层位置,我相应地更新了代码。此外,更改层的转换会触发直接父级中的 layoutSubviews。我忘了这个。解决此问题的最简单方法是将指针层放入作为 SpeedDial 子视图的 UIView。我已经更新了代码。祝你好运!也许这有点矫枉过正,但它比对视图、背景和所有内容的整个渲染进行动画处理更具可重用性。