1. 程式人生 > IOS開發 >畫個圓動畫,的兩種實現。iOS 動畫由很淺,入淺,當然是 Swift

畫個圓動畫,的兩種實現。iOS 動畫由很淺,入淺,當然是 Swift

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

相關程式碼