如何在 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() 将使其使用新的指针位置重绘,在initialValuetargetValue 之间进行插值

【问题讨论】:

您想通过此属性更改实现什么目标?如果该属性更改了在 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 配置导航属性

在快速静态错误中为自定义 UIView 设置动画

在为自定义 DataGridViewColumn 设置属性时访问 DataGridView 控件

无法使用 animator() [ OSX ] 为 Swift 自定义属性设置动画