目前在自己的個人項目裡,已經開始使用Swift去編寫代碼。這篇文章把項目中自己設計的一個ActivityIndicator View展示給大家。
在開始之前,我們先看看最終的效果,如下圖:
我建議大家下載本文對應在Github分享的完整項目,以便跟著本篇文章來閱讀代碼。
需求分析
我們需要實現一個自定義的和 UIActivityIndicatorView 提供相似功能的一個Loading效果。我們將使用 Core Graphics 來繪制這樣的效果,並讓它動起來。
讓我們先分析一下這個控件的組成,為我們實際編碼提供具體的思路。
首先,這個loading效果圖,是由8個圓弧組成的一個圓。
我們先要會畫圓弧:
像這樣畫8個圓弧,圍成一個圓:
然後通過重復改變每一個圓弧的顏色,讓它動起來。
我們繼承UIView, 重寫drawRect方法繪制界面,第一步得到當前繪圖的上下文:
let context = UIGraphicsGetCurrentContext()
繪制圓弧
這裡我們使用 UIBezierPath 類去構建路徑,然後通過繪制路徑的方式繪制圓弧。
// 初始化一個 UIBezierPath 實例 let arcPath = UIBezierPath() // 構建Arc路徑 arcPath.addArcWithCenter(CGPointMake(CGFloat(self.frame.size.width/2), CGFloat(self.frame.size.height/2)), radius: CGFloat(Config.CC_ARC_DRAW_RADIUS), startAngle: CGFloat(DegreesToRadians(startAngle)), endAngle: CGFloat(DegreesToRadians(startAngle + Config.CC_ARC_DRAW_DEGREE)), clockwise: true) // 把路徑添加到當前繪圖的上下文 CGContextAddPath(context, arcPath.CGPath) // 設置線段寬度 CGContextSetLineWidth(context, CGFloat(Config.CC_ARC_DRAW_WIDTH)) // 設置線段顏色 CGContextSetStrokeColorWithColor(context, strokeColor) // 繪制 CGContextStrokePath(context)
通過如上的方式,我們就可以成功畫出一個圓弧。其中:
func addArcWithCenter(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool)
這個方法構建路徑的解釋是 center 為圓點坐標,radius 為半徑,startAngle 為開始的弧度,endAngle 為結束的弧度,clockwise 表示的是順時針還是逆時針。
繪制8個圓弧
當我們可以成功在繪圖上下文繪制出圓弧時,我們應該開始著手繪制效果圖中的8個圓弧,並讓它在正確的位置,並帶上不同顏色。
這裡是效果圖的一些參數設置,包括半徑,寬度,顏色等信息:
struct Config { static let CC_ACTIVITY_INDICATOR_VIEW_WIDTH = 40 static let CC_ARC_DRAW_PADDING = 3.0 static let CC_ARC_DRAW_DEGREE = 39.0 static let CC_ARC_DRAW_WIDTH = 6.0 static let CC_ARC_DRAW_RADIUS = 10.0 static let CC_ARC_DRAW_COLORS = [UIColor(red: 242/255.0, green: 242/255.0, blue: 242/255.0, alpha: 1.0).CGColor, UIColor(red: 230/255.0, green: 230/255.0, blue: 230/255.0, alpha: 1.0).CGColor, UIColor(red: 179/255.0, green: 179/255.0, blue: 179/255.0, alpha: 1.0).CGColor, UIColor(red: 128/255.0, green: 128/255.0, blue: 128/255.0, alpha: 1.0).CGColor, UIColor(red: 128/255.0, green: 128/255.0, blue: 128/255.0, alpha: 1.0).CGColor, UIColor(red: 128/255.0, green: 128/255.0, blue: 128/255.0, alpha: 1.0).CGColor, UIColor(red: 128/255.0, green: 128/255.0, blue: 128/255.0, alpha: 1.0).CGColor, UIColor(red: 128/255.0, green: 128/255.0, blue: 128/255.0, alpha: 1.0).CGColor] }
我們可以在drawRect方法,循壞繪制8個圓弧,此時完整的代碼看上去像這樣:
override func drawRect(rect: CGRect) { let context = UIGraphicsGetCurrentContext() var startAngle = Config.CC_ARC_DRAW_PADDING for index in 1...8 { let arcPath = UIBezierPath() arcPath.addArcWithCenter(CGPointMake(CGFloat(self.frame.size.width/2), CGFloat(self.frame.size.height/2)), radius: CGFloat(Config.CC_ARC_DRAW_RADIUS), startAngle: CGFloat(DegreesToRadians(startAngle)), endAngle: CGFloat(DegreesToRadians(startAngle + Config.CC_ARC_DRAW_DEGREE)), clockwise: true) CGContextAddPath(context, arcPath.CGPath) startAngle += Config.CC_ARC_DRAW_DEGREE + (Config.CC_ARC_DRAW_PADDING * 2) CGContextSetLineWidth(context, CGFloat(Config.CC_ARC_DRAW_WIDTH)) let colorIndex = abs(index - self.animateIndex) let strokeColor = Config.CC_ARC_DRAW_COLORS[colorIndex] CGContextSetStrokeColorWithColor(context, strokeColor) CGContextStrokePath(context) } }
使用for循環繪制8次,產生8個圓弧,並且設置不同的顏色。這裡的self.animateIndex用來跟蹤整個動畫的頭一個顏色最淺圓弧的位置。通過它和當前index的絕對值,獲得當前圓弧應該顯示的顏色。
動起來
在設計一個ActivityIndicator View的時候,我們應該像UIKit提供的 UIActivityIndicatorView 一樣,至少需要實現這三組API:
func startAnimating() func stopAnimating() func isAnimating() -> Bool
這裡我們使用一個timer去改變self.animateIndex的值,不斷重畫當前視圖,來產生動畫效果,代碼看起來像這樣:
// 使用該值驅動改變圓弧顏色,產生動畫效果 private var animateIndex: Int = 1 // 動畫的Timer private var animatedTimer: NSTimer? // timer響應的事件,在這裡setNeedsDisplay讓UIKit重畫當前視圖,然後不斷改變animateIndex值。 @objc private func animate () { if !self.hidden { self.setNeedsDisplay() self.animateIndex++ if self.animateIndex > 8 { self.animateIndex = 1 } } } // 開始動畫 func startAnimating () { if self.hidden { self.hidden = false } if let timer = self.animatedTimer { timer.fire() } else { self.animatedTimer = NSTimer(timeInterval: 0.1, target: self, selector: "animate", userInfo: nil, repeats: true) NSRunLoop.currentRunLoop().addTimer(self.animatedTimer!, forMode: NSRunLoopCommonModes) } }
這裡使用
init(timeInterval ti: NSTimeInterval, target aTarget: AnyObject, selector aSelector: Selector, userInfo: AnyObject?, repeats yesOrNo: Bool) -> NSTimer
而不是使用
class func scheduledTimerWithTimeInterval(ti: NSTimeInterval, target aTarget: AnyObject, selector aSelector: Selector, userInfo: AnyObject?, repeats yesOrNo: Bool) -> NSTimer
構建timer的原因是:當我們在使用自己的ActivityIndicator View的時候,我們可能把它放到UIScrollView上面。這個時候使用scheduledTimerWithTimeInterval創建的timer是加入到當前Run Loop中的,而UIScrollView在接收到用戶交互事件時,主線程Run Loop會設置為UITrackingRunLoopMode。這個時候會導致timer失效。更詳細的解答,我在走進Run Loop的世界 (一):什麼是Run Loop?一文中有說明。
總結
到這個時候,我們應該就能看到和效果圖一樣的動畫效果。但是寫一個可供使用的自定義控件時,應該考慮更多的細節工作。比如初始化,視圖移除,intrinsicContentSize,是否需要支持 @IBInspectable 和 @IBDesignable 等等,來讓使用我們控件的開發者更加友好。更加詳細的代碼和Demo可以去這裡查看:https://github.com/yechunjun/CCActivityIndicatorView