加上 CABasicAnimation
有一个动画属性 strokeEnd
就算完
func draw(in ctx: CGContext)
也是可以的通过定制 CALayer, 还要有一个使用该定制 CALayer 的 custom 视图。
使用 @NSManaged
, 方便自定制的 CALayer
键值观察 KVC
重写 CALayer
的方法 action(forKey:)
, 指定需要的动画
重写 CALayer
的方法 needsDisplay(forKey:)
, 先指定刷新渲染,再出 action(forKey:)
的动画
class CircleView: UIView { let circleLayer: CAShapeLayer = { // 形状图层,初始化与属性配置 let circle = CAShapeLayer() circle.fillColor = UIColor.clear.cgColor circle.strokeColor = UIColor.red.cgColor circle.lineWidth = 5.0 circle.strokeEnd = 0.0 return circle }() // 视图创建,通过指定 frame override init(frame: CGRect) { super.init(frame: frame) setup() } // 视图创建,通过指定 storyboard required init?(coder: NSCoder) { super.init(coder: coder) setup() } func setup(){ backgroundColor = UIColor.clear // 添加上,要动画的图层 layer.addSublayer(circleLayer) } override func layoutSubviews() { super.layoutSubviews() // 考虑到视图的布局,如通过 auto layout, // 需动画图层的布局,放在这里 let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(Double.pi * 2.0), clockwise: true) circleLayer.path = circlePath.cgPath } // 动画的方法 func animateCircle(duration t: TimeInterval) { // 画圆形,就是靠 `strokeEnd` let animation = CABasicAnimation(keyPath: "strokeEnd") // 指定动画时长 animation.duration = t // 动画是,从没圆,到满圆 animation.fromValue = 0 animation.toValue = 1 // 指定动画的时间函数,保持匀速 animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) // 视图具体的位置,与动画结束的效果一致 circleLayer.strokeEnd = 1.0 // 开始动画 circleLayer.add(animation, forKey: "animateCircle") } } 复制代码
class ViewController: UIViewController { // storyboard 布局 @IBOutlet weak var circleV: CircleView! @IBAction func animateFrame(_ sender: UIButton) { let diceRoll = CGFloat(Int(arc4random_uniform(7))*30) let circleEdge = CGFloat(200) // 直接指定 frame 布局 let circleView = CircleView(frame: CGRect(x: 50, y: diceRoll, width: circleEdge, height: circleEdge)) view.addSubview(circleView) // 开始动画 circleView.animateCircle(duration: 1.0) } @IBAction func animateAutolayout(_ sender: UIButton) { // auto layout 布局 let circleView = CircleView(frame: CGRect.zero) circleView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(circleView) circleView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true circleView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true circleView.widthAnchor.constraint(equalToConstant: 250).isActive = true circleView.heightAnchor.constraint(equalToConstant: 250).isActive = true // 开始动画 circleView.animateCircle(duration: 1.0) } @IBAction func animateStoryboard(_ sender: UIButton) { // 开始动画 circleV.animateCircle(duration: 1.0) } } 复制代码
先要自定制一个基于 CAShapeLayer 的图层
对 @NSManaged var val: CGFloat
KVC,
触发 override class func needsDisplay(forKey key: String) -> Bool
,
调用 setNeedsDisplay()
,重新渲染,
接着触发 override func action(forKey event: String) -> CAAction?
,
指定动画,
频繁调用绘制方法 override func draw(in ctx: CGContext)
, 就是可见的动画
@NSManaged
关键字,类似 Objective-C 里面的 @dynamic
关键字@NSManaged
关键字,方便键值编码
@NSManaged
通知编译器,不要初始化,运行时保证有值
override class func needsDisplay(forKey key: String) -> Bool
返回 true就是需要重新渲染,调用 setNeedsDisplay()
方法
下面的
override class func needsDisplay(forKey key: String) -> Bool { if key == "val" { return true } else { return super.needsDisplay(forKey: key) } } 复制代码
相当于
override class func needsDisplay(forKey key: String) -> Bool { if key == "val" { return true } else { return false } } 复制代码
override func action(forKey event: String) -> CAAction?
, 返回协议对象 CAAction
CAAnimation
遵守 CAAction
协议,这里一般返回个 CAAnimation
一个 CALayer
图层,可以有动态的动画行为。
发起动画时,可以设置该图层的动画属性,操作关联出来的具体动画
下面的
override func action(forKey event: String) -> CAAction? { if event == "val"{ // 实际动画部分 let animation = CABasicAnimation(keyPath: "val") // ... return animation } else { return super.action(forKey: event) } } 复制代码
相当于
override func action(forKey event: String) -> CAAction? { if event == "val"{ // 实际动画部分 let animation = CABasicAnimation(keyPath: "val") // ... return animation } else { return nil } } 复制代码
/** 动画起作用的枢纽, 负责处理绘制和动画, 对于使用者隐藏,使用者操作外部的视图类就好 */ class UICircularRingLayer: CAShapeLayer { // MARK: 属性 @NSManaged var val: CGFloat let ringWidth: CGFloat = 20 let startAngle = CGFloat(-90).rads // MARK: 初始化 override init() { super.init() } override init(layer: Any) { // 确保使用姿势 guard let layer = layer as? UICircularRingLayer else { fatalError("unable to copy layer") } super.init(layer: layer) } required init?(coder aDecoder: NSCoder) { return nil } // MARK: 视图渲染部分 /** 重写 draw(in 方法,画圆环 */ override func draw(in ctx: CGContext) { super.draw(in: ctx) UIGraphicsPushContext(ctx) // 画圆环 drawRing(in: ctx) UIGraphicsPopContext() } // MARK: 动画部分 /** 监听 val 属性的变化,重新渲染 */ override class func needsDisplay(forKey key: String) -> Bool { if key == "val" { return true } else { return super.needsDisplay(forKey: key) } } /** 监听 val 属性的变化,指定动画行为 */ override func action(forKey event: String) -> CAAction? { if event == "val"{ // 实际动画部分 let animation = CABasicAnimation(keyPath: "val") animation.fromValue = presentation()?.value(forKey: "val") animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) animation.duration = 2 return animation } else { return super.action(forKey: event) } } /** 画圆,通过路径布局。主要是指定 UIBezierPath 曲线的角度 */ private func drawRing(in ctx: CGContext) { let center: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY) let radiusIn: CGFloat = (min(bounds.width, bounds.height) - ringWidth)/2 // 开始画 let innerPath: UIBezierPath = UIBezierPath(arcCenter: center, radius: radiusIn, startAngle: startAngle, endAngle: toEndAngle, clockwise: true) // 具体路径 ctx.setLineWidth(ringWidth) ctx.setLineJoin(.round) ctx.setLineCap(CGLineCap.round) ctx.setStrokeColor(UIColor.red.cgColor) ctx.addPath(innerPath.cgPath) ctx.drawPath(using: .stroke) } // 本例子中,起始角度固定,终点角度通过 val 设置 var toEndAngle: CGFloat { return (val * 360.0).rads + startAngle } } 复制代码
extension CGFloat { var rads: CGFloat { return self * CGFloat.pi / 180 } } 复制代码
自定制 UIView,指定其图层为,之前的定制图层
@IBDesignable open class UICircularRing: UIView { /** 将 UIView 自带的 layer,强转为上面的 UICircularRingLayer, 方便使用 */ var ringLayer: UICircularRingLayer { return layer as! UICircularRingLayer } /** 将 UIView 自带的 layer,重写为 UICircularRingLayer */ override open class var layerClass: AnyClass { return UICircularRingLayer.self } /** 通过 frame 初始化,的设置 */ override public init(frame: CGRect) { super.init(frame: frame) setup() } /** 通过 storyboard 初始化,的设置 */ required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setup() } /** 初始化的配置 */ func setup(){ // 设置光栅化 // 将光栅化后的内容缓存起来,方便复用 ringLayer.contentsScale = UIScreen.main.scale ringLayer.shouldRasterize = true ringLayer.rasterizationScale = UIScreen.main.scale * 2 ringLayer.masksToBounds = false backgroundColor = UIColor.clear ringLayer.backgroundColor = UIColor.clear.cgColor ringLayer.val = 0 } func startAnimation() { ringLayer.val = 1 } } 复制代码
class ViewController: UIViewController { let progressRing = UICircularRing(frame: CGRect(x: 100, y: 100, width: 250, height: 250)) override func viewDidLoad() { super.viewDidLoad() view.addSubview(progressRing) } @IBAction func animate(_ sender: UIButton) { progressRing.startAnimation() } } 复制代码
ctx.setLineCap(CGLineCap.round)