画个圆动画,的两种实现。iOS 动画由很浅,入浅,当然是 Swift

4,152 阅读4分钟

1

方法一,使用 CAShapeLayer 和 UIBezierPath

加上 CABasicAnimation 有一个动画属性 strokeEnd

就算完

方法二,复杂一些。频繁调用 CALayer 的 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)

    }

}

方法二的实现

核心类 UICircularRingLayer 的技术注意:

先要自定制一个基于 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()
    }


}
方法二,设置线条帽,为圆头,比较方便

444

ctx.setLineCap(CGLineCap.round)

iOS 设置角度的坐标图

777

相关代码