【问题标题】:Animate an expanding drawing in iOS UIView Swift在 iOS UIView Swift 中为扩展绘图设置动画
【发布时间】:2017-12-28 19:01:55
【问题描述】:

我想使用 CGContext 在 UIView 内创建自定义动态绘图,这样当我在其上执行 UIView 动画时,绘图将在动画/插值期间呈现。我尝试使用自定义 CALayer 并覆盖其绘制方法,但失败了。我尝试将 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 中的任何其他自定义绘图)设置动画?

编辑:这是我创建的一个自定义类,仅用于绘制一个三角形。当我为其边界设置动画时,三角形会随着视图的底层图像缩放,但在动画期间不会调用 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()
}

}

【问题讨论】:

    标签: ios swift animation uiview calayer


    【解决方案1】:

    我一直在解决您的问题,这种方法是使用 CABasicAnimationCAShapeLayer 我已经定义了一个自定义的 UIView 类来完成这项工作,我将添加进一步的改进 @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
        }
        */
    
    }
    

    结果

    希望对你有帮助

    【讨论】:

    • 这是一个替代答案。你知道为什么在我原来的问题中,在 UIView animate() 期间没有调用 oberriden draw() 方法吗?
    • 感谢您为这个答案付出的辛勤工作,但这不是我要寻找的答案——制作一个不断扩大的圆圈应该不难。必须有一个更简单的解决方案,使用 UIView.animate() 静态方法。
    【解决方案2】:

    用这个最简单的方法

    完整示例代码

    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.
        }
    
    
    }
    

    结果

    希望对你有帮助

    【讨论】:

    • 也许我应该试着画一个三角形——你能改变你的答案来动画一个扩展的三角形吗?实际上,我正在尝试为自定义绘图设置动画,圆圈只是一个示例。
    • 对于三角形必须一样工作,你只需要在你的 UIView 中自定义绘制一个三角形,它必须工作,如果不工作告诉我好吗?
    • 在示例中如何编码绘制三角形?我尝试覆盖 draw() 方法,但不起作用。
    • @andrewz 看看
    • 我明白你做了什么——这看起来正是我想要的样子。然而,这与我理解的解决方案应该如何工作不同。绘图不是重新创建的,而是重新绘制的,但这很好。谢谢。
    【解决方案3】:

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

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

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

    例子:

    //
    //  ViewController.swift
    //  StackOverflow
    //
    //  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。

    【讨论】:

    • 感谢您为这个答案付出的辛勤工作,但这不是我要寻找的答案——制作一个不断扩大的圆圈应该不难。必须有一个更简单的解决方案,使用 UIView.animate() 静态方法。
    • @andrewz;我添加了使用 UIView.animateWIthDuration 的代码我通过设置 contentMode 解决了模糊问题。
    • 是的,我希望在每一帧都调用 draw() ......现在我意识到使用 CADisplayLink 是实现这一目标的唯一方法。我没有意识到 UIKit 在动画期间的工作方式是它试图通过不重新创建每一帧的绘图来进行优化,而是通过对已经合成的图像执行转换并重新绘制像贝塞尔曲线这样的动态绘图。我猜想重新绘制一张图需要重新分配一张图片,而且成本会很高。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-07-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-02-06
    相关资源
    最近更新 更多