臟矩形(高效繪圖 13.3)
臟矩形
有時候用CAShapeLayer
或者其他矢量圖形圖層替代Core Graphics並不是那麽切實可行。比如我們的繪圖應用:我們用線條完美地完成了矢量繪制。但是設想一下如果我們能進一步提高應用的性能,讓它就像一個黑板一樣工作,然後用『粉筆』來繪制線條。模擬粉筆最簡單的方法就是用一個『線刷』圖片然後將它粘貼到用戶手指碰觸的地方,但是這個方法用CAShapeLayer
沒辦法實現。
我們可以給每個『線刷』創建一個獨立的圖層,但是實現起來有很大的問題。屏幕上允許同時出現圖層上線數量大約是幾百,那樣我們很快就會超出的。這種情況下我們沒什麽辦法,就用Core Graphics吧(除非你想用OpenGL做一些更復雜的事情)。
我們的『黑板』應用的最初實現見清單13.3,我們更改了之前版本的DrawingView
,用一個畫刷位置的數組代替UIBezierPath
。圖13.2是運行結果
清單13.3 簡單的類似黑板的應用
1 #import "DrawingView.h" 2 #import 3 #define BRUSH_SIZE 32 4 5 @interface DrawingView () 6 7 @property (nonatomic, strong) NSMutableArray *strokes; 8 9 @end 10 11 @implementation DrawingViewView Code12 13 - (void)awakeFromNib 14 { 15 //create array 16 self.strokes = [NSMutableArray array]; 17 } 18 19 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 20 { 21 //get the starting point 22 CGPoint point = [[touches anyObject] locationInView:self]; 23 24 //add brush stroke25 [self addBrushStrokeAtPoint:point]; 26 } 27 28 - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 29 { 30 //get the touch point 31 CGPoint point = [[touches anyObject] locationInView:self]; 32 33 //add brush stroke 34 [self addBrushStrokeAtPoint:point]; 35 } 36 37 - (void)addBrushStrokeAtPoint:(CGPoint)point 38 { 39 //add brush stroke to array 40 [self.strokes addObject:[NSValue valueWithCGPoint:point]]; 41 42 //needs redraw 43 [self setNeedsDisplay]; 44 } 45 46 - (void)drawRect:(CGRect)rect 47 { 48 //redraw strokes 49 for (NSValue *value in self.strokes) { 50 //get point 51 CGPoint point = [value CGPointValue]; 52 53 //get brush rect 54 CGRect brushRect = CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE); 55 56 //draw brush stroke ? 57 [[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect]; 58 } 59 } 60 @end
圖13.2 用程序繪制一個簡單的『素描』
這個實現在模擬器上表現還不錯,但是在真實設備上就沒那麽好了。問題在於每次手指移動的時候我們就會重繪之前的線刷,即使場景的大部分並沒有改變。我們繪制地越多,就會越慢。隨著時間的增加每次重繪需要更多的時間,幀數也會下降(見圖13.3),如何提高性能呢?
圖13.3 幀率和線條質量會隨時間下降。
為了減少不必要的繪制,Mac OS和iOS設備將會把屏幕區分為需要重繪的區域和不需要重繪的區域。那些需要重繪的部分被稱作『臟區域』。在實際應用中,鑒於非矩形區域邊界裁剪和混合的復雜性,通常會區分出包含指定視圖的矩形位置,而這個位置就是『臟矩形』。
當一個視圖被改動過了,TA可能需要重繪。但是很多情況下,只是這個視圖的一部分被改變了,所以重繪整個寄宿圖就太浪費了。但是Core Animation通常並不了解你的自定義繪圖代碼,它也不能自己計算出臟區域的位置。然而,你的確可以提供這些信息。
當你檢測到指定視圖或圖層的指定部分需要被重繪,你直接調用-setNeedsDisplayInRect:
來標記它,然後將影響到的矩形作為參數傳入。這樣就會在一次視圖刷新時調用視圖的-drawRect:
(或圖層代理的-drawLayer:inContext:
方法)。
傳入-drawLayer:inContext:
的CGContext
參數會自動被裁切以適應對應的矩形。為了確定矩形的尺寸大小,你可以用CGContextGetClipBoundingBox()
方法來從上下文獲得大小。調用-drawRect()
會更簡單,因為CGRect
會作為參數直接傳入。
你應該將你的繪制工作限制在這個矩形中。任何在此區域之外的繪制都將被自動無視,但是這樣CPU花在計算和拋棄上的時間就浪費了,實在是太不值得了。
相比依賴於Core Graphics為你重繪,裁剪出自己的繪制區域可能會讓你避免不必要的操作。那就是說,如果你的裁剪邏輯相當復雜,那還是讓Core Graphics來代勞吧,記住:當你能高效完成的時候才這樣做。
清單13.4 展示了一個-addBrushStrokeAtPoint:
方法的升級版,它只重繪當前線刷的附近區域。另外也會刷新之前線刷的附近區域,我們也可以用CGRectIntersectsRect()
來避免重繪任何舊的線刷以不至於覆蓋已更新過的區域。這樣做會顯著地提高繪制效率(見圖13.4)
清單13.4 用-setNeedsDisplayInRect:
來減少不必要的繪制
1 - (void)addBrushStrokeAtPoint:(CGPoint)point 2 { 3 //add brush stroke to array 4 [self.strokes addObject:[NSValue valueWithCGPoint:point]]; 5 6 //set dirty rect 7 [self setNeedsDisplayInRect:[self brushRectForPoint:point]]; 8 } 9 10 - (CGRect)brushRectForPoint:(CGPoint)point 11 { 12 return CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE); 13 } 14 15 - (void)drawRect:(CGRect)rect 16 { 17 //redraw strokes 18 for (NSValue *value in self.strokes) { 19 //get point 20 CGPoint point = [value CGPointValue]; 21 22 //get brush rect 23 CGRect brushRect = [self brushRectForPoint:point]; 24 ? 25 //only draw brush stroke if it intersects dirty rect 26 if (CGRectIntersectsRect(rect, brushRect)) { 27 //draw brush stroke 28 [[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect]; 29 } 30 } 31 }View Code
臟矩形(高效繪圖 13.3)