文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

iOS坐标系的深入探究

2022-05-15 12:16

关注

前言

app在渲染视图时,需要在坐标系中指定绘制区域。

这个概念看似乎简单,事实并非如此。

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的背景设置成黑色:


- (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),在左上角进行了绘制。

UILabel绘制

接着,我们尝试使用CoreText来渲染一段文本。


- (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常用的画图来参考,当我们使用画笔🖌在白板中写字时,图形上下文就是画笔的属性设置、白板大小、画笔位置等等。

iOS中,每个图形上下文都会有三种坐标:

绘制坐标系(也叫用户坐标系),我们平时绘制所用的坐标系;

视图(view)坐标系,固定左上角为原点(0,0)的view坐标系;

物理坐标系,物理屏幕中的坐标系,同样是固定左上角为原点;

根据我们绘制的目标不同(屏幕、位图、PDF等),会有多个context;

Quartz常见的绘制目标

不同context的绘制坐标系各不相同,比如说UIKit的坐标系为左上角原点的坐标系,CoreGraphics的坐标系为左下角为原点的坐标系;

CoreGraphics坐标系和UIKit坐标系的转换

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];

遇到的问题

1、UILabel.layer在renderInContext的时候frame失效

初始化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);

2、renderInContext和drawInContext的选择

在把UILabel.layer渲染到context的时候,应该采用drawInContext还是renderInContext?

虽然这两个方法都可以生效,但是根据画线部分的内容来判断,还是采用了renderInContext,并且问题1就是由这里的一句Renders in the coordinate space of the layer,定位到问题所在。

3、如何理解CoreGraphics坐标系不一致后,会出现绘制结果异常?

我的理解方法是,我们可以先不考虑坐标系变换的情况。

如下图,上半部分是普通的渲染结果,可以很容易的想象;

接下来是增加坐标变换后,坐标系变成原点在左上角的顶点,相当于按照下图的虚线进行了一次垂直的翻转。

也可以按照坐标系变换的方式去理解,将左下角原点的坐标系相对y轴做一次垂直翻转,然后向上平移height的高度,这样得到左上角原点的坐标系。

附录

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对编程网的支持。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯