如何在 iOS 中为自定义属性设置动画
Posted
技术标签:
【中文标题】如何在 iOS 中为自定义属性设置动画【英文标题】:How to animate a custom property in iOS 【发布时间】:2017-01-09 04:06:14 【问题描述】:我有一个自定义 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,但它会立即更改。对动画设置器有两次调用,一次调用值为 0.5,然后立即调用值为 1.0。如果我将要制作动画的属性更改为标准 UIView 属性,例如alpha,它可以正常工作。
我来自 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
之间进行插值
【问题讨论】:
您想通过此属性更改实现什么目标?如果该属性更改了在 draw(in: rect) 中执行的绘图的某些方面,您将无法像那样对其进行动画处理。 @twiz_ 看来如此。关于其他方法的任何建议? 看看使用CAAnimation
修改自定义绘制矩形图!我不确定UIView.animate
方式是否适用于自定义绘制矩形图!
@Clyde,如果我知道您想要制作动画的内容,即视图的哪个方面正在发生变化,我确实会这样做。你有之前和之后的图片或一般描述吗?很可能最终您将不得不将动画方面放入 CALayer 并对图层进行动画处理。此外,func animate
的代码会有所帮助
@twiz_ 我更详细地更新了问题
【参考方案1】:
简答:使用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
代码可以被改进和概括,但它正在做我需要的工作。
【讨论】:
【参考方案2】:如果这完全是用 CoreGraphics 绘制的,那么如果你想做一些数学运算,那么有一种非常简单的方法来制作动画。幸运的是,你有一个刻度,可以告诉你精确旋转的弧度数,所以数学是最小的,不涉及三角函数。这样做的好处是您不必重绘整个背景,甚至指针。获得正确的角度和东西可能有点棘手,如果以下方法不起作用,我可以提供帮助。
通常在draw(矩形)中绘制视图的背景。您应该放入 CALayer 的指针。您几乎可以将指针的绘制代码(包括中心深灰色圆圈)移动到返回 UIImage 的单独方法中。该层将被调整为视图的框架(在布局子视图中),并且锚点必须设置为 (0.5, 0.5),这实际上是默认值,因此您应该可以将那条线排除在外。然后你的动画方法只是根据你的需要改变图层的变换来旋转。这就是我将如何做到的。我将更改方法和变量名称,因为anime 和animate 有点太晦涩了。
由于图层属性以 0.25 的持续时间隐式进行动画处理,您甚至可以在不调用动画方法的情况下逃脱。自从我使用 CoreAnimation 以来已经有一段时间了,所以显然要测试一下。
这里的优点是您只需将表盘的 RPM 设置为您想要的,它就会旋转到该速度。没有人会读你的代码,就像 WTF 是 _anime 一样! :) 我已经包含了 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。我已经更新了代码。祝你好运!也许这有点矫枉过正,但它比对视图、背景和所有内容的整个渲染进行动画处理更具可重用性。
【讨论】:
感谢您的详细回答。是的,这一切都是用 Core Graphics 完成的,并且 CGContext 被缩放和平移,因此我在以针为中心的 100x100 框中进行绘制。动画数值怎么样?无论如何,这需要重新绘制每个动画周期。此外,在同一个 UIView 中有多个这样的仪表,所有仪表都同时更新,因此将它们一起制作动画是有意义的。添加多个子视图会变得一团糟——现在只有一个类来绘制仪表,而且它非常轻量级,每个仪表只有一个参数化实例。 我还有其他需要动画的元素,这是标准 UIView 或图层属性无法完成的 - 例如改变长度和颜色的条形。 没有汗水。改变长度和颜色的条形最好作为 CAShapeLayers 进行动画处理。更改 RPM 文本 - 我只需将 UILabel 子视图添加到快速拨号,并相应定位,然后更改 rpm 变量的 didSet 子句中的值。 听起来你已经准备好了所有这些,所以上面的代码需要相当多的工作,在这种情况下,CADisplayLink 可能会更好。但是更改 RPM 值或多个仪表不会有太大问题。与每次重绘整个仪表相比,仅更改图层的变换会少很多。 我怀疑使用子视图会节省很多性能 - 仪表背景是预先绘制的并呈现为图像,绘制针只是两个简单的路径和一个椭圆。无论采用哪种方式,无论如何都应该将其交给图形处理器。难题在于缺乏像 Android 那样的通用动画框架。该应用程序是 already working on Android 使用CADisplayLink
可以正常工作,但它有点 hack。【参考方案3】:
如果您在gauge.animate(newValue)
函数中修改任何自动布局约束,则需要调用layoufIfNeeded()
而不是setNeedsDisplay()
。
https://***.com/a/12664093/255549
【讨论】:
布局没有被修改。setNeedsDisplay()
触发对 draw()
的调用,这就是更新显示所需的全部内容。以上是关于如何在 iOS 中为自定义属性设置动画的主要内容,如果未能解决你的问题,请参考以下文章
如何在 .NET 中为自定义配置部分启用 configSource 属性?
在 UITableViewCell 中为自定义 UIButton 设置动画
如何在 EF Core 5 中为自定义 SQL 配置导航属性