加上 CABasicAnimation
有一個動畫屬性 strokeEnd
git
就算完github
func draw(in ctx: CGContext)
也是能夠的經過定製 CALayer, 還要有一個使用該定製 CALayer 的 custom 視圖。緩存
使用 @NSManaged
, 方便自定製的 CALayer
鍵值觀察 KVCbash
重寫 CALayer
的方法 action(forKey:)
, 指定須要的動畫dom
重寫 CALayer
的方法 needsDisplay(forKey:)
, 先指定刷新渲染,再出 action(forKey:)
的動畫ide
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()
,從新渲染,ui
接着觸發 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)