在 iOS UIView Swift 中为展开的绘图设置动画

Animate an expanding drawing in iOS UIView Swift

我想使用 CGContext 在 UIView 中创建自定义动态绘图,这样当我在其上执行 UIView 动画时,绘图将在 animation/interpolation 期间呈现。我尝试使用自定义 CALayer 并覆盖其绘制方法,但失败了。我试过 subclassing a UIView draw(_ in:CGRect) 但它只是第一次被绘制。这就是我想要做的:

class RadioWaveView : UIView {
    override func draw(_ in:CGRect) {
        // draw something, for example an ellipse based on frame size, something like this:
        // let context = CGGraphicsCurrentContext()
        // context.addEllipse(self.frame)
        // Unfortunately this method is called only once, not during animation
    }
}

viewDidAppear() 中:

UIView.animate(withDuration: 5, delay: 1, options: [.repeat], animations: {
        self.myRadioWaveView.frame = CGRect(x:100,y:100,width:200,heigh:300)
    }, completion: { flag in
    })

动画之所以有效,是因为我设置了 myRadioWaveView 的背景色并看到它在屏幕上展开。 draw下断点表示只执行一次

编辑:本质上,我在问这个问题:我们如何创建自定义 UIView 并使用 UIView.animate() 来为该视图的渲染设置动画?让我们保留 UIView.animate() 方法,我们如何为扩大的圆圈(或 UIView 中的任何其他自定义绘图)设置动画?

编辑:这是我创建的自定义 class 来绘制一个三角形。当我为其边界设置动画时,三角形随视图的底层图像缩放,但在动画期间未调用 draw 方法。我不想要那个;我希望 draw() 方法在动画的每一帧重绘绘图。

open class TriangleView : UIView {

override open func draw(_ rect: CGRect) {
    let context = UIGraphicsGetCurrentContext()!
    let path = CGMutablePath()
    path.move(to: CGPoint.zero)
    path.addLine(to: CGPoint(x:self.frame.width,y:0))
    path.addLine(to: CGPoint(x:self.frame.width/2,y:self.frame.height))
    path.addLine(to: CGPoint.zero)
    path.closeSubpath()
    context.beginPath()
    context.setStrokeColor(UIColor.white.cgColor)
    context.setLineWidth(3)
    context.addPath(path)
    context.closePath()
    context.strokePath()
}

}

两个答案合二为一。首先,如果您真的希望在每次传递时调用 drawRect,您应该使用 CADisplayLink 并手动为视图的框架设置动画。 第二:如果你不想这样做但仍然希望它绘制流畅,那么将 contentMode 设置为 scaleAspectFit。这将使它在 UIView.animateWithDuration 下平滑地绘制。但是,它不会像 CADisplayLink 解决方案那样在每个周期调用 drawRect

使用每帧更新边界的CADisplayLink。添加 needsDisplayOnBoundsChange = true 以便在边界更改时重新绘制视图。

http://i.imgur.com/JjarYsq.gif

示例:

//
//  ViewController.swift
//  Whosebug
//
//  Created by Brandon T on 2017-07-22.
//  Copyright © 2017 XIO. All rights reserved.
//

import UIKit

class RadioWave : UIView {

    private var dl: CADisplayLink?
    private var startTime: CFTimeInterval!
    private var fromFrame: CGRect!
    private var toFrame: CGRect!
    private var duration: CFTimeInterval!
    private var completion: (() -> Void)?

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.isOpaque = false
        self.layer.needsDisplayOnBoundsChange = true;
        self.fromFrame = frame;
        self.toFrame = frame
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func animateToFrame(frame: CGRect, duration: CFTimeInterval, completion:(() -> Void)? = nil) {
        self.dl?.remove(from: .current, forMode: .commonModes)
        self.dl?.invalidate()
        self.dl = CADisplayLink(target: self, selector: #selector(onUpdate(dl:)))

        self.completion = completion
        self.fromFrame = self.frame
        self.toFrame = frame
        self.duration = duration
        self.startTime = CACurrentMediaTime()
        self.dl?.add(to: .current, forMode: .commonModes)
    }

    override func draw(_ rect: CGRect) {
        let ctx = UIGraphicsGetCurrentContext()
        ctx?.clear(rect)

        ctx?.addEllipse(in: rect)
        ctx?.clip()
        ctx?.setFillColor(UIColor.blue.cgColor)
        ctx?.fill(rect)
    }

    @objc
    func onUpdate(dl: CADisplayLink) {
        let dt = CGFloat((dl.timestamp - self.startTime) / self.duration)

        if (dt > 1.0) {
            self.frame = self.toFrame
            self.dl?.remove(from: .current, forMode: .commonModes)
            self.dl?.invalidate()
            self.dl = nil

            completion?()
            completion = nil
            return;
        }

        var frame: CGRect! = self.toFrame;
        frame.origin.x = (self.toFrame.origin.x - self.fromFrame.origin.x) * dt + fromFrame.origin.x
        frame.origin.y = (self.toFrame.origin.y - self.fromFrame.origin.y) * dt + fromFrame.origin.y
        frame.size.width = (self.toFrame.size.width - self.fromFrame.size.width) * dt + fromFrame.size.width
        frame.size.height = (self.toFrame.size.height - self.fromFrame.size.height) * dt + fromFrame.size.height
        self.frame = frame
    }
}

class ViewController: UIViewController {

    var radioWave: RadioWave!

    override func viewDidLoad() {
        super.viewDidLoad()

        self.radioWave = RadioWave(frame: CGRect(x: self.view.center.x - 10.0, y: self.view.center.y - 10.0, width: 20.0, height: 20.0))

        self.view.addSubview(self.radioWave)

        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            let smallFrame = CGRect(x: self.view.center.x - 10.0, y: self.view.center.y - 10.0, width: 20.0, height: 20.0)
            let largeFrame = CGRect(x: self.view.center.x - 100.0, y: self.view.center.y - 100.0, width: 200.0, height: 200.0)

            self.radioWave.animateToFrame(frame: largeFrame, duration: 2.0, completion: {

                self.radioWave.animateToFrame(frame: smallFrame, duration: 2.0)

                })
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

视图通常只在边界改变时才重绘。 iOS 上的动画实际上只是您的视图被插值(表示层)的快照。这大大提高了性能,而不是在每个动画步骤上重新绘制和布置视图。

使用 [UIView animateWithDuration]

制作圆圈动画
class RadioWave : UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.isOpaque = false
        self.layer.needsDisplayOnBoundsChange = true
        self.contentMode = .scaleAspectFit
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func draw(_ rect: CGRect) {
        let ctx = UIGraphicsGetCurrentContext()
        ctx?.clear(rect)

        ctx?.addEllipse(in: rect)
        ctx?.clip()
        ctx?.setFillColor(UIColor.blue.cgColor)
        ctx?.fill(rect)
    }
}

class ViewController: UIViewController {

    var radioWave: RadioWave!

    override func viewDidLoad() {
        super.viewDidLoad()

        self.radioWave = RadioWave(frame: CGRect(x: self.view.center.x - 10.0, y: self.view.center.y - 10.0, width: 20.0, height: 20.0))

        self.view.addSubview(self.radioWave)

        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            let largeFrame = CGRect(x: self.view.center.x - 100.0, y: self.view.center.y - 100.0, width: 200.0, height: 200.0)

            self.radioWave.setNeedsDisplay()

            UIView.animate(withDuration: 2.0, delay: 0.0, options: [.layoutSubviews, .autoreverse, .repeat], animations: {

                self.radioWave.frame = largeFrame
            }, completion: { (completed) in

            })
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

上面使用了 contentMode scaleAspectFit 可以让动画适当缩放而不模糊!这是我所知道的唯一方法,可以在动画时绘制平滑的圆圈。它不会调用 drawRect 直到每个动画的开始和结束。

我一直在研究你的问题,这种方法是使用 CABasicAnimationCAShapeLayer 我定义了一个自定义 UIView Class 来完成工作,我将添加进一步的改进 @IBDesignable@IBInspectables

import UIKit

class RadioWaveAnimationView: UIView {

    var animatableLayer : CAShapeLayer?

    override func awakeFromNib() {
        super.awakeFromNib()
        self.layer.cornerRadius = self.bounds.height/2

        self.animatableLayer = CAShapeLayer()
        self.animatableLayer?.fillColor = self.backgroundColor?.cgColor
        self.animatableLayer?.path = UIBezierPath(roundedRect: self.bounds, cornerRadius: self.layer.cornerRadius).cgPath
        self.animatableLayer?.frame = self.bounds
        self.animatableLayer?.cornerRadius = self.bounds.height/2
        self.animatableLayer?.masksToBounds = true
        self.layer.addSublayer(self.animatableLayer!)
        self.startAnimation()
    }


    func startAnimation()
    {
        let layerAnimation = CABasicAnimation(keyPath: "transform.scale")
        layerAnimation.fromValue = 1
        layerAnimation.toValue = 3
        layerAnimation.isAdditive = false

        let layerAnimation2 = CABasicAnimation(keyPath: "opacity")
        layerAnimation2.fromValue = 1
        layerAnimation2.toValue = 0
        layerAnimation2.isAdditive = false

        let groupAnimation = CAAnimationGroup()
        groupAnimation.animations = [layerAnimation,layerAnimation2]
        groupAnimation.duration = CFTimeInterval(2)
        groupAnimation.fillMode = kCAFillModeForwards
        groupAnimation.isRemovedOnCompletion = true
        groupAnimation.repeatCount = .infinity

        self.animatableLayer?.add(groupAnimation, forKey: "growingAnimation")
    }
    /*
    // Only override draw() if you perform custom drawing.
    // An empty implementation adversely affects performance during animation.
    override func draw(_ rect: CGRect) {
        // Drawing code
    }
    */

}

结果

希望对您有所帮助

用这个最简单的方法

完整示例代码

import UIKit

class ViewController: UIViewController {
    
    
    @IBOutlet weak var animatableView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        //Circle
        //animatableView.layer.cornerRadius = animatableView.bounds.size.height/2
        //animatableView.layer.masksToBounds = true
        //triangle
        self.applyTriangle(givenView: animatableView)
        
        UIView.animate(withDuration: 2, delay: 1, options: [.repeat], animations: {
            self.animatableView.transform = CGAffineTransform(scaleX: 2, y: 2)
        }) { (finished) in
            self.animatableView.transform = CGAffineTransform.identity
        }
        
    }
    
    func applyTriangle(givenView: UIView){
        
        let bezierPath = UIBezierPath()
        bezierPath.move(to: CGPoint(x: givenView.bounds.width / 2, y: givenView.bounds.width))
        bezierPath.addLine(to: CGPoint(x: 0, y: 0))
        bezierPath.addLine(to: CGPoint(x: givenView.bounds.width, y: 0))
        bezierPath.close()
        
        let shapeLayer = CAShapeLayer(layer: givenView.layer)
        shapeLayer.path = bezierPath.cgPath
        shapeLayer.frame = givenView.bounds
        shapeLayer.masksToBounds = true
        givenView.layer.mask = shapeLayer
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


}

结果

希望对您有所帮助