iOS坐标系的深入探究
前言
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中,每个图形上下文都会有三种坐标:
1、绘制坐标系(也叫用户坐标系),我们平时绘制所用的坐标系;
2、视图(view)坐标系,固定左上角为原点(0,0)的view坐标系;
3、物理坐标系,物理屏幕中的坐标系,同样是固定左上角为原点;
根据我们绘制的目标不同(屏幕、位图、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的高度,这样得到左上角原点的坐标系。
附录
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。