iOS開發日記52-CALayer與coreAnimation,calayeranimation
今天博主有一個CALayer與coreAnimation的需求,遇到了一些困痛點,在此和大家分享,希望能夠共同進步.
進度條常規做法
進度條並不是單純的線性增長,在50%之前,每一次進度增加,進度條就會在y軸上面位移一段距離,直到增長到一半進度的時候位移位置達到頂點,然後隨著進度繼續增加,y軸的位移越來越小,直到變回一條直線。
從實現角度而言,使用CAShapeLayer
然後在每次進度改變的時候更新其path
值就能夠實現。如果使用CAShapeLayer
的方式,我們需要建立兩個執行個體對象,一個放在下面作為進度條背景,另一個在上面隨著進度改變而改變。圖示如下:
每次進度發生改變的時候,我們都要根據當前進度計算出進度座標位置,然後更新兩個圖層的path
,代碼如下:
- (void)updatePath{ UIBezierPath * path = [UIBezierPath bezierPath]; [path moveToPoint: CGPointMake(25, 150)]; [path addLineToPoint: CGPointMake((CGRectGetWidth([UIScreen mainScreen].bounds) - 50) * _progress + 25, 150 + (25.f * (1 - fabs(_progress - 0.5) * 2)))]; [path addLineToPoint: CGPointMake(CGRectGetWidth([UIScreen mainScreen].bounds) - 25, 150)]; self.background.path = path.CGPath; self.top.path = path.CGPath; self.top.strokeEnd = _progress;}
事實上,使用這種方式實現進度效果的時候,進度會比直接在當前上下文繪製的響應上要慢上幾幀,即是我們肉眼可以看到這種延時更新的效果,是不利於使用者體驗的。其次,我們需要額外建立一個背景圖層,在記憶體上有了額外的花銷。
自訂layer
這小節我們要通過自訂CALayer
的子類來實現上面的進度條效果,我們需要對外開放progress屬性。每次這個值發生改變的時候我們要調用[self setNeedsDisplay]
來重新繪製進度條
@property(nonatomic, assign) CGFloat progress;
重寫setter方法,檢測進度值範圍以及重新繪製進度條
- (void)setProgress: (CGFloat)progress{ _progress = MIN(1.f, MAX(0.f, progress)); [self setNeedsDisplay];}
重新回顧一下進度條,我們可以把進度條分成兩條線,分別是綠色的已完成進度條和灰色的進度條。根據進度條的不同,分為<0.5, =0.5, >0.5三種狀態:
從可知,在進度達到一半的時候,我們的進度條在Y軸上的位移量達到最大值。因此,我們應當定義一個最大位移值MAX_OFFSET。
#define MAX_OFFSET 25.f
另一方面,當前進度條的y軸位移量是根據進度按比例進行位移的。在我們改變進度_progress的時候,重新繪製進度條。下面是綠色進度條的繪製
- (void)drawInContext: (CGContextRef)ctx{ CGFloat offsetX = _origin.x + MAX_LENGTH * _progress; CGFloat offsetY = _origin.y + _maxOffset * (1 - fabs((_progress - 0.5f) * 2)); CGMutablePathRef mPath = CGPathCreateMutable(); CGPathMoveToPoint(mPath, NULL, _origin.x, _origin.y); CGPathAddLineToPoint(mPath, NULL, offsetX, offsetY); CGContextAddPath(ctx, mPath); CGContextSetStrokeColorWithColor(ctx, [UIColor greenColor].CGColor); CGContextSetLineWidth(ctx, 5.f); CGContextSetLineCap(ctx, kCGLineCapRound); CGContextStrokePath(ctx); CGPathRelease(mPath);}
ps: 這裡存在一個很重要的問題,自訂的layer必須加在我們自訂的view上面,才能實現drawInContext:方法進行不斷的重繪。關於coreGraphics相關方法的更多使用,請參考這篇文章
第二部分的灰色線條基於當前位移的座標為起點進行繪製,在這裡有兩個小陷阱:
- 不熟練的開發人員很容易直接把繪製灰色線條的代碼放在上面這段代碼的後面。這樣會導致灰色線條在綠色線條後面繪製而將綠色線條遮住了一部分使得綠色線條端末非圓形
- 沒有對_progress的值進行判斷。當_progress為0時,上面的代碼也會線上條左側產生一個綠色小圓點,這是不準確的。
因此,我們在確定好當前進度對應的位移座標時,應該直接繪製灰色線條,再繪製綠色進度條。在繪製綠色線條前應當對_progress進行一次判斷
- (void)drawInContext: (CGContextRef)ctx{ CGFloat offsetX = _origin.x + MAX_LENGTH * _progress; CGFloat offsetY = _origin.y + _maxOffset * (1 - fabs((_progress - 0.5f) * 2)); CGMutablePathRef mPath = CGPathCreateMutable(); CGPathMoveToPoint(mPath, NULL, offsetX, offsetY); CGPathAddLineToPoint(mPath, NULL, _origin.x + MAX_LENGTH, _origin.y); CGContextAddPath(ctx, mPath); CGContextSetStrokeColorWithColor(ctx, [UIColor lightGrayColor].CGColor); CGContextSetLineWidth(ctx, 5.f); CGContextSetLineCap(ctx, kCGLineCapRound); CGContextStrokePath(ctx); CGPathRelease(mPath); if (_progress != 0.f) { mPath = CGPathCreateMutable(); CGPathMoveToPoint(mPath, NULL, _origin.x, _origin.y); CGPathAddLineToPoint(mPath, NULL, offsetX, offsetY); CGContextAddPath(ctx, mPath); CGContextSetStrokeColorWithColor(ctx, [UIColor greenColor].CGColor); CGContextSetLineWidth(ctx, 5.f); CGContextSetLineCap(ctx, kCGLineCapRound); CGContextStrokePath(ctx); CGPathRelease(mPath); }}
這時候在controller裡面加上一個UISlider拖拉來控制你的進度條進度,看看是不是想要的效果完成了。
擴充
上面我們在實現繪製的時候,對填充色彩顏色是寫死的,這樣不利於代碼擴充。回顧CAShapeLayer,在繼承CALayer的基礎上添加了fillColor、strokeColor等類似屬性,我們可以通過添加類似的成員屬性來完成封裝,這裡我們需要為進度條添加兩個屬性,分別表示進度條顏色跟背景顏色
@property(nonatomic, assign) CGColorRef backgroundColor;@property(nonatomic, assign) CGColorRef strokeColor;
我們在設定顏色的時候直接傳入color.CGColor就可以完成賦值了,我們把上面的設定顏色代碼分別改成下面所示後重新運行
CGContextSetStrokeColorWithColor(ctx, _backgroundColor);CGContextSetStrokeColorWithColor(ctx, _strokeColor);
有的朋友們會發現一個坑爹的事情,崩潰了,出現了EXC_BAD_ACCESS
錯誤——如果你使用系統提供的[UIColor xxxColor].CGColor
,那麼這裡不會出問題。
這是因為我們增加的兩個屬性為assign類型,在我們使用這個color的時候,它早就被釋放了。由這裡我們可以看到兩件事情:
- CAShapeLayer會對非對象且屬於coreGraphics的屬性進行橋接或者引用操作
- [UIColor xxxColor]方法返回的對象應該是全域或者靜態對象。為了節省記憶體消耗,應該是使用懶載入方式。有必要的情況下,可以不調用這些方法來實現最佳化記憶體的效果
因此,我們應該重寫這兩個屬性的setter方法來實現引用(歡迎來到MRC)
- (void)setStrokeColor: (CGColorRef)strokeColor{ CGColorRelease(_strokeColor); _strokeColor = strokeColor; CGColorRetain(_strokeColor); [self setNeedsDisplay];}
除此之外,CAShapeLayer還有一個有趣的屬性strokeEnd
,這個屬性決定了整個圖層有多少部分需要被渲染的。想查看這個屬性的看官們可以在最開始的常規代碼中為layer設定這個屬性,然後你會發現這時候不管我們的progress設定為多少,進度條的綠色部分總是等同於strokeEnd
。效果如所示
可以看到,基於strokeEnd進行繪製的時候,介面的繪製難度更加複雜了。但是我們同樣可以把這個拆分,分為兩種情況
1、strokeEnd>progress
這個情況對應圖中上面兩個圖,當然,在progress=1跟progress=0的狀態是一樣的。可以看到,當progress不為零的時候,進度條分為三部分:
- 位移點左側的綠色線條
- 右側多出的綠色線條
- 最後的灰色線條
交接點的y座標應當是由strokeEnd超出progress的百分比部分除以當前右側總長度佔線條總長度的百分比,如所示
因此我們需要判斷兩個座標點,其中位移點按照上面代碼一樣根據progress得出,計算背景色和進度顏色交接點的代碼如下:
CGFloat contactX = _origin.x + MAX_LENGTH * _strokeEnd;CGFloat contactY = _origin.y + (offsetY - _origin.y) * ((1 - (_strokeEnd - _progress) / (1 - _progress)));
2、strokeEnd<=progress
這時候就對應下面的兩張圖了,同樣的,我們可以把進度條拆分成三部分:
- 最左側的綠色進度條
- 處於進度條和位移點中間的背景顏色條
- 右側的背景顏色條
按照上面的圖解方式進行分析,相當於把右側的位置資訊放到了左側,我們可以輕易的得出顏色交接點座標的計算方式
CGFloat contactX = _origin.x + MAX_LENGTH * _strokeEnd;CGFloat contactY = (offsetY - _origin.y) * (_progress == 0 ?: _strokeEnd / _progress) + _origin.y;
有了上面的解析計算,drawInContext
的代碼如下
- (void)drawInContext: (CGContextRef)ctx{ CGFloat offsetX = _origin.x + MAX_LENGTH * _progress; CGFloat offsetY = _origin.y + _maxOffset * (1 - fabs(_progress - 0.5f) * 2); CGFloat contactX = 25.f + MAX_LENGTH * _strokeEnd; CGFloat contactY = _origin.y + _maxOffset * (1 - fabs(_strokeEnd - 0.5f) * 2); CGRect textRect = CGRectOffset(_textRect, MAX_LENGTH * _progress, _maxOffset * (1 - fabs(_progress - 0.5f) * 2)); if (_report) { _report((NSUInteger)(_progress * 100), textRect, _strokeColor); } CGMutablePathRef linePath = CGPathCreateMutable(); //繪製背景線條 if (_strokeEnd > _progress) { CGFloat scale = _progress == 0 ?: (1 - (_strokeEnd - _progress) / (1 - _progress)); contactY = _origin.y + (offsetY - _origin.y) * scale; CGPathMoveToPoint(linePath, NULL, contactX, contactY); } else { CGFloat scale = _progress == 0 ?: _strokeEnd / _progress; contactY = (offsetY - _origin.y) * scale + _origin.y; CGPathMoveToPoint(linePath, NULL, contactX, contactY); CGPathAddLineToPoint(linePath, NULL, offsetX, offsetY); } CGPathAddLineToPoint(linePath, NULL, _origin.x + MAX_LENGTH, _origin.y); [self setPath: linePath onContext: ctx color: [UIColor colorWithRed: 204/255.f green: 204/255.f blue: 204/255.f alpha: 1.f].CGColor]; CGPathRelease(linePath); linePath = CGPathCreateMutable(); //繪製進度線條 if (_progress != 0.f) { CGPathMoveToPoint(linePath, NULL, _origin.x, _origin.y); if (_strokeEnd > _progress) { CGPathAddLineToPoint(linePath, NULL, offsetX, offsetY); } CGPathAddLineToPoint(linePath, NULL, contactX, contactY); } else { if (_strokeEnd != 1.f && _strokeEnd != 0.f) { CGPathMoveToPoint(linePath, NULL, _origin.x, _origin.y); CGPathAddLineToPoint(linePath, NULL, contactX, contactY); } } [self setPath: linePath onContext: ctx color: [UIColor colorWithRed: 66/255.f green: 1.f blue: 66/255.f alpha: 1.f].CGColor]; CGPathRelease(linePath);}
我們把添加CGPathRef以及設定線條顏色、大小等參數的代碼封裝成setPath: onContext: color
方法,以此來減少代碼量。
http://www.cocoachina.com/ios/20141022/10005.html?1413946650
http://www.cnblogs.com/wendingding/p/3800736.html