欢迎你们前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~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 送你一份技术课程大礼包!
海量技术实践经验,尽在云加社区!