歡迎你們前往騰訊雲+社區,獲取更多騰訊海量技術實踐乾貨哦~javascript
本文由 落影發表於 雲+社區專欄
app在渲染視圖時,須要在座標系中指定繪製區域。 這個概念看彷佛簡單,事實並不是如此。html
When an app draws something in iOS, it has to locate the drawn content in a two-dimensional space defined by a coordinate system. This notion might seem straightforward at first glance, but it isn’t.
咱們先從一段最簡單的代碼入手,在drawRect中顯示一個普通的UILabel; 爲了方便判斷,我把整個view的背景設置成黑色:java
- (void)drawRect:(CGRect)rect { [super drawRect:rect]; CGContextRef context = UIGraphicsGetCurrentContext(); NSLog(@"CGContext default CTM matrix %@", NSStringFromCGAffineTransform(CGContextGetCTM(context))); UILabel *testLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 100, 28)]; testLabel.text = @"測試文本"; testLabel.font = [UIFont systemFontOfSize:14]; testLabel.textColor = [UIColor whiteColor]; [testLabel.layer renderInContext:context]; }
這段代碼首先建立一個UILabel,而後設置文本,顯示到屏幕上,沒有修改座標。 因此按照UILabel.layer默認的座標(0, 0),在左上角進行了繪製。app
UILabel繪製機器學習
接着,咱們嘗試使用CoreText來渲染一段文本。ide
- (void)drawRect:(CGRect)rect { [super drawRect:rect]; CGContextRef context = UIGraphicsGetCurrentContext(); NSLog(@"CGContext default matrix %@", NSStringFromCGAffineTransform(CGContextGetCTM(context))); NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:@"測試文本" attributes:@{ NSForegroundColorAttributeName:[UIColor whiteColor], NSFontAttributeName:[UIFont systemFontOfSize:14], }]; CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef) attrStr); // 根據富文本建立排版類CTFramesetterRef UIBezierPath * bezierPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 100, 20)]; CTFrameRef frameRef = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), bezierPath.CGPath, NULL); // 建立排版數據 CTFrameDraw(frameRef, context); }
首先用NSString建立一個富文本,而後根據富文本建立CTFramesetterRef,結合CGRect生成的UIBezierPath,咱們獲得CTFrameRef,最終渲染到屏幕上。 可是結果與上文不一致:文字是上下顛倒。學習
CoreText的文本繪製測試
從這個不一樣的現象開始,咱們來理解iOS的座標系。字體
在iOS中繪製圖形必須在一個二維的座標系中進行,但在iOS系統中存在多個座標系,常須要處理一些座標系的轉換。 先介紹一個圖形上下文(graphics context)的概念,好比說咱們經常使用的CGContext就是Quartz 2D的上下文。圖形上下文包含繪製所需的信息,好比顏色、線寬、字體等。用咱們在Windows經常使用的畫圖來參考,當咱們使用畫筆🖌在白板中寫字時,圖形上下文就是畫筆的屬性設置、白板大小、畫筆位置等等。ui
iOS中,每一個圖形上下文都會有三種座標: 一、繪製座標系(也叫用戶座標系),咱們平時繪製所用的座標系; 二、視圖(view)座標系,固定左上角爲原點(0,0)的view座標系; 三、物理座標系,物理屏幕中的座標系,一樣是固定左上角爲原點;
根據咱們繪製的目標不一樣(屏幕、位圖、PDF等),會有多個context;
Quartz常見的繪製目標
不一樣context的繪製座標系各不相同,好比說UIKit的座標系爲左上角原點的座標系,CoreGraphics的座標系爲左下角爲原點的座標系;
CoreText基於CoreGraphics,因此座標系也是CoreGraphics的座標系。 咱們回顧下上文提到的兩個渲染結果,咱們產生以下疑問: UIGraphicsGetCurrentContext返回的是CGContext,表明着是左下角爲原點的座標系,用UILabel(UIKit座標系)能夠直接renderInContext,而且「測」字對應爲UILabel的(0,0)位置,是在左上角? 當用CoreText渲染時,座標是(0,0),可是渲染的結果是在左上角,並非在左下角;而且文字是上下顛倒的。 爲了探究這個問題,我在代碼中加入了一行log: NSLog(@"CGContext default matrix %@", NSStringFromCGAffineTransform(CGContextGetCTM(context)));
其結果是CGContext default matrix [2, 0, 0, -2, 0, 200]
; CGContextGetCTM返回是CGAffineTransform仿射變換矩陣:
一個二維座標系上的點p,能夠表達爲(x, y, 1),乘以變換的矩陣,以下:
把結果相乘,獲得下面的關係
此時,咱們再來看看打印的結果[2, 0, 0, -2, 0, 200],能夠化簡爲 x' = 2x, y' = 200 - 2y 由於渲染的view高度爲100,因此這個座標轉換至關於把原點在左下角(0,100)的座標系,轉換爲原點在左上角(0,0)的座標系!一般咱們都會使用UIKit進行渲染,因此iOS系統在drawRect返回CGContext的時候,默認幫咱們進行了一次變換,以方便開發者直接用UIKit座標系進行渲染。
咱們嘗試對系統添加的座標變換進行還原: 先進行CGContextTranslateCTM(context, 0, self.bounds.size.height);
對於x' = 2x, y' = 200 - 2y,咱們使得x=x,y=y+100;(self.bounds.size.height=100) 因而有x' = 2x, y' = 200-2(y+100) = -2y; 再進行CGContextScaleCTM(context, 1.0, -1.0);
對於x' = 2x, y' = -2y,咱們使得x=x, y=-y; 因而有 x'=2x, y' = -2(-y) = 2y;
- (void)drawRect:(CGRect)rect { [super drawRect:rect]; CGContextRef context = UIGraphicsGetCurrentContext(); CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM(context, 1.0, -1.0); NSLog(@"CGContext default matrix %@", NSStringFromCGAffineTransform(CGContextGetCTM(context))); NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:@"測試文本" attributes:@{ NSForegroundColorAttributeName:[UIColor whiteColor], NSFontAttributeName:[UIFont systemFontOfSize:14], }]; CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef) attrStr); // 根據富文本建立排版類CTFramesetterRef UIBezierPath * bezierPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 100, 20)]; CTFrameRef frameRef = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), bezierPath.CGPath, NULL); // 建立排版數據 CTFrameDraw(frameRef, context); }
經過log也能夠看出來CGContext default matrix [2, 0, -0, 2, 0, 0];
最終結果以下,文本從左下角開始渲染,而且沒有出現上下顛倒的狀況。
這時咱們產生新的困擾: 用CoreText渲染文字的上下顛倒現象解決,可是修改後的座標系UIKit沒法正常使用,如何兼容兩種座標系? iOS可使用CGContextSaveGState()
方法暫存context狀態,而後在CoreText繪製完後經過CGContextRestoreGState ()
能夠恢復context的變換。
- (void)drawRect:(CGRect)rect { [super drawRect:rect]; CGContextRef context = UIGraphicsGetCurrentContext(); NSLog(@"CGContext default matrix %@", NSStringFromCGAffineTransform(CGContextGetCTM(context))); CGContextSaveGState(context); CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM(context, 1.0, -1.0); NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:@"測試文本" attributes:@{ NSForegroundColorAttributeName:[UIColor whiteColor], NSFontAttributeName:[UIFont systemFontOfSize:14], }]; CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef) attrStr); // 根據富文本建立排版類CTFramesetterRef UIBezierPath * bezierPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 100, 20)]; CTFrameRef frameRef = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), bezierPath.CGPath, NULL); // 建立排版數據 CTFrameDraw(frameRef, context); CGContextRestoreGState(context); NSLog(@"CGContext default CTM matrix %@", NSStringFromCGAffineTransform(CGContextGetCTM(context))); UILabel *testLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 100, 20)]; testLabel.text = @"測試文本"; testLabel.font = [UIFont systemFontOfSize:14]; testLabel.textColor = [UIColor whiteColor]; [testLabel.layer renderInContext:context]; }
渲染結果以下,控制檯輸出的兩個matrix都是[2, 0, 0, -2, 0, 200]
;
初始化UILabel時設定了frame,可是沒有生效。 UILabel *testLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 100, 28)];
這是由於frame是在上一層view中座標的偏移,在renderInContext中座標起點與frame無關,因此須要修改的是bounds屬性: testLabel.layer.bounds = CGRectMake(50, 50, 100, 28);
在把UILabel.layer渲染到context的時候,應該採用drawInContext仍是renderInContext?
雖然這兩個方法均可以生效,可是根據畫線部分的內容來判斷,仍是採用了renderInContext,而且問題1就是由這裏的一句Renders in the coordinate space of the layer
,定位到問題所在。
個人理解方法是,咱們能夠先不考慮座標系變換的狀況。 以下圖,上半部分是普通的渲染結果,能夠很容易的想象; 接下來是增長座標變換後,座標系變成原點在左上角的頂點,至關於按照下圖的虛線進行了一次垂直的翻轉。
也能夠按照座標系變換的方式去理解,將左下角原點的座標系相對y軸作一次垂直翻轉,而後向上平移height的高度,這樣獲得左上角原點的座標系。
Drawing and Printing Guide for iOS Quartz 2D Programming Guide
相關閱讀
【每日課程推薦】機器學習實戰!快速入門在線廣告業務及CTR相應知識
此文已由做者受權騰訊雲+社區發佈,更多原文請點擊
搜索關注公衆號「雲加社區」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!
海量技術實踐經驗,盡在雲加社區!