在 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 直到每个动画的开始和结束。
我一直在研究你的问题,这种方法是使用 CABasicAnimation
和 CAShapeLayer
我定义了一个自定义 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.
}
}
结果
希望对您有所帮助
我想使用 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 直到每个动画的开始和结束。
我一直在研究你的问题,这种方法是使用 CABasicAnimation
和 CAShapeLayer
我定义了一个自定义 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.
}
}
结果
希望对您有所帮助