您的位置:首页 > 产品设计 > UI/UE

二:UIKit与Quartz 2D绘图技术学习

2016-01-26 17:43 459 查看
文中内容均来自《IOS图形图像、动画和多媒体编程技术最佳实践》,若有侵犯版权,请告知笔者,笔者必将删除

写在前面的话:

在IOS中绘图技术主要包括:UIKit、Quartz 2D、Core Animation和OpenGL ES。其中Core Animation提供动画实现技术,OpenGL ES是OpenGL针对嵌入式设备的简化版本,用以绘制高性能的2D和3D图形。这里主要UIKit和Quartz 2D。

UIKit。它是高级别的图形接口,它的API都是基于Objective-C的。它能够访问绘图、动画、字体、图片等内容。

Quartz 2D。是IOS和Mac OS X环境下的2D绘图引擎。涉及内容包括:基于路径的绘图,透明度绘图,遮盖,阴影,透明层,颜色管理,防锯齿渲染,生成PDF,以及PDF元数据相关处理。Quartz 2D也被称为Core Graphics,缩写前缀为CG。Quartz 2D与Quartz Compositor统称为Quartz,Quartz原本是Mac OS X的Darwin核心之上的绘图技术。它的API接口都是基于C的。

1.1 绘制视图

在IOS上无论采用哪种绘图技术(UIKit、Quartz 2D、Core Animation和OpenGL ES),都离不开UIView,绘制都发生在UIView对象的区域内。在绘制发生的时候如果使用的是系统提供的视图,绘制工作会自动得到处理。然而,如果是自定义视图,则必须重写drawRect:方法,在此提供相应的绘制代码。

1.1.1 绘制视图

在IOS绘制的时候比较麻烦,不会简单地调用一个方法久可以绘制出来了。而是首先为需要绘制的视图或者视图的部分区域设置一个需要绘制的标志,在事件循环的每一轮中,绘图引擎会检查是否有需要更新的内容,如果有就会调用视图的drawRect:方法进行绘制,因此我们需要绘制的视图中重写drawRect:方法。

一旦drawRect:方法被调用,就可以使用任何的UIKit、Quartz 2D、OpenGL ES等技术对视图的内容进行绘制了。

绘图过程中除了使用了drawRect:方法,还有setNeedsDisplay和setNeedsDisplayInRect:。setNeedsDisplay和setNeedsDisplayInRect:方法是设置视图或者视图部分区域是否需要重新绘制,setNeedsDisplay是重新绘制整个视图,setNeedsDisplayInRect是重新绘制视图的部分区域。原则上,尽量不要绘制视图的全部,以减少绘制带来开销。触发视图重新绘制的动作有如下几种:

当遮挡你的视图的其他视图被移动或删除操作的时候;

将视图的hidden属性声明设置为NO,使其从隐藏状态变为可见;

将视图滚出屏幕,然后再重新回到屏幕上;

显式调用视图的setNeedsDisplay或者setNeedsDisplayInRect:方法

1.1.2 绘制视图

- (void)drawRect:(CGRect)rect {
[[UIColor brownColor] setFill];
UIRectFill(rect);
}


这两条语句都属于UIKit提供的,其中[[UIColor brownColor] setFill]语句是为当前的图形上下文设置需要填充的颜色。函数UIRectFill(rect)是按照刚才设置的颜色进行填充矩形。

1.1.3 填充与描边

UIKit提供非常基本的绘图功能,主要的API有:

- UIRectFill(CGRect rect),填充矩形函数

- UIRectFrame(CGRect rect),矩形描边函数

- UIBezierPath,绘制常见路径类,包括险段、渐变、阴影、反锯齿等高级特性支持还是不及Quartz 2D。

- (void)drawRect:(CGRect)rect {
// Drawing code
[[UIColor brownColor] setFill];
UIRectFill(rect);

[[UIColor whiteColor] setStroke];
CGRect frame = CGRectMake(20, 30, 100, 300);
UIRectFrame(frame);
}


其中[[UIColor whiteColor] setStroke];是设置图形上下文白色描边。

1.1.4 绘制图像和文本

除了可以绘制几何图形外,也可以绘制文本和图像。这就像是使用Interface Builder工具从对象库中拖拽出来的标准控件(UILabel和UIImageView)一样。这些绘制可以使用UIImage和NSString实现。它们对应的绘制方法如下。

UIImage类中绘制图像主要的方法:

-drawAtPoint:(CGPoint)point,设置绘制定点

-drawInRect:(CGRect)rect,图片绘制在指定的矩形里

-drawAsPatternInRect:(CGRet)rect,在指定的矩形里平铺绘制图片,如果图片大小超出了指定的矩形,形式上与-drawAtPoint:方法类似了,如果图片大小小于指定的矩形,就会有平铺的效果

NSString类中绘制文本主要的方法:

-(void)drawAtPoint:(CGPoint)point withAttributes:(NSDicitionary *)attires,文本在指定点绘制

-(void)drawInRect:(CGRect)rect withAttributes:(NSDictionary *)attires,文本在指定的矩形里绘制

下面通过一个实例介绍图像和文本的绘制,界面中看到的图片和文字都不是通过标准控件设计出来的,而是通过绘制方法绘制出来的。

- (void)drawRect:(CGRect)rect {
// Drawing code
NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"dog" ofType:@"png"];
UIImage *myImageObj = [[UIImage alloc] initWithContentsOfFile:imagePath];

[myImageObj drawInRect:CGRectMake(0, 40, 320, 400)];

NSString *s = @"我的小狗";

UIFont *font = [UIFont systemFontOfSize:34];
NSDictionary *attr = @{NSFontAttributeName:font};
[s drawAtPoint:CGPointMake(100, 20) withAttributes:attr];
}


注意:[myImageObj drawInRect:CGRectMake(0, 40, 320, 400)];语句把图片放在矩形中显示,由于矩形高宽比例与图片中的原始比例不一样导致图片变形,这是使用drawInRect:方法进行绘制图片时需要注意的。

[s drawAtPoint:CGPointMake(100, 20) withAttributes:attr];绘制字符串,一般不使用,而是使用UILabel控件。

2.2 Quartz 图形上下文

关于图形上下文的讲解,作者形象生动的比喻让人很容易理解,假如想作画,图像上下文便是你的手,你想画红色,就拿起红色画笔,想画绿色就拿起绿色画笔。当切换图形上下文的颜色及其它参数时,就是在替换不同的蜡笔。前面的实例里[UIColor blueColor],就是拿起蓝色的画笔,我们还可以给图形上下文设置很多变量赋值,从而改变绘制的样式。

图形上下文包括绘制系统执行后,绘制命令所需要的信息,定义了各种基本的绘制参数,比如绘制使用的颜色、裁剪区域、线段的宽度及风格信息、字体信息、合成选项以及几个其他信息。

在调用drawRect:方法之前,视图对象会自动配置其绘制环境,使代码立即执行进行绘制。作为这些配置的一部分,UIView对象会对当前绘制环境创建一个图形上下文(对应于CGContextRef封装类型)。在前面的实例中就是采用这种默认方式的图形上下文。

我们也可以在drawRect:方法中通过(CGContextRef)UIGraphicsGetCurrentContext(void)函数获得访问图形上下文对象。图形上下文仅对当前的drawRect:方法使用有效,不要把图形上下文对象设置为成员变量。

让我们通过一个简单实例了解一下视图控制过程。我们在整个视图中绘制一个红色三角形:

- (void)drawRect:(CGRect)rect {
// 创建图形上下文对象
CGContextRef context = UIGraphicsGetCurrentContext();
// 相关路径
CGContextMoveToPoint(context, 75, 10);
CGContextAddLineToPoint(context, 10, 150);
CGContextAddLineToPoint(context, 160, 150);
CGContextClosePath(context);

// 设置黑色描边参数
[[UIColor blackColor] setStroke];
// 设置红色条填充参数
[[UIColor redColor] setFill];
// 绘制路径
CGContextDrawPath(context, kCGPathFillStroke);
}


有的时候需要多次改编图形上下文对象的参数,这样两次绘制就可能互相有影响,这就好像拿着蜡笔画画,每一次我的手只能拿一支蜡笔。为了防止互相影响,需要CGContextSaveGState函数保存图形上下文设置,绘制完成我们再使用CGContextRestoreGState函数回复图形上下文设置。例如:

- (void)drawRect:(CGRect)rect {
// 创建图形上下文对象
CGContextRef context = UIGraphicsGetCurrentContext();
// 相关路径
CGContextMoveToPoint(context, 75, 10);
CGContextAddLineToPoint(context, 10, 150);
CGContextAddLineToPoint(context, 160, 150);
CGContextClosePath(context);

// 设置黑色描边参数
[[UIColor blackColor] setStroke];
// 设置绿色条填充参数
[[UIColor greenColor] setFill];

// 保存上下文参数
CGContextSaveGState(context);
// 设置红色填充参数
[[UIColor redColor] setFill];

// 恢复之前的上下文参数
CGContextRestoreGState(context);
// 绘制路径
CGContextDrawPath(context, kCGPathFillStroke);
}


最后绘制出来的三角形填充的颜色为绿色

2.3 Quartz 路径

我们用路径来描述矩形、圆及其他想要画的2D几何图形。通过路径可以对这些几何图形进行描边、填充和描边填充处理。Core Graphics(Quartz 2D)中有4个基本图元用于描述路径:点、线段、弧和贝塞尔曲线。

1.点

点是二维空间中的一个位置,不要把它想成像素,一个点完全不占空间,所依画一个点不会在屏幕上显示任何东西,我们可以在路径里加入很多的点,相加多少加多少。

2.线段

线段由两个点定义:起点和终点。线段可以通过描边绘制出来,我们可以通过设置图形上下文,如画笔宽度或者颜色等参数,就可以绘制出两点之间的线段。线段没有面积,所依它们不能被填充。我们可以用一组线段或者曲线组成一个具有闭合路径的几何图形,然后填充它。

3.弧

弧可以由一个圆心点、半径、起始角和结束角描述的。圆是弧的特例,只需要设置为起始角0度,结束角为360度就可以了,因为弧是占有一定面积的路径,所以可以被填充、描边和描边填充出来。

4.贝塞尔曲线

贝塞尔曲线是法国数学家在工作中发现,任何一条曲线都可以通过与它相切的控制线两端的点的位置来定义。因为,贝塞尔曲线可以用4个点描述,其中两个点描述两个端点,另外两个描述每一端的切线。贝塞尔可以分为:二次方贝塞尔曲线和高阶贝塞尔曲线(三次方贝塞尔曲线)。



CGContentMoveToPoint(content,75,10);

CGContextAddLineToPoint(context,10,150);

CGContextAddLineToPoint(context,160,150);

CGContextClosePath(context);

其中,CGContentMoveToPoint(content,75,10)语句是在(75,10)绘制起始点。CGContextAddLineToPoint(context,10,150)是绘制从(75,10)到(10,150)线段。CGContextAddLineToPoint(context,160,150)是绘制从(10,150)到(160,150)到线段。这样通过上面3句语句就定义了一个三角形路径,如图2-11所示,路径是按照(75,10) -> (10,150) -> (160,150)顺序定义的,因此它不是闭合的。如果需要闭合可以调用CGContextClosePath(context)函数实现,如图2-12所示。

定义和绘制路径是两个不同的操作,先定义路径,再绘制它。CGContextDrawPath(context,kCGPathFillStroke)函数实现 绘制路径,其中kCGPathFillStroke,参数是填充描边处理,此外还有:kCGPathFill和kCGPathStroke,分别代表填充和描边处理。



如图2-13是贝塞尔曲线的实例的效果。所示为半个瓶子的轮廓,可以分成很多贝塞尔曲线,然后找到贝塞尔曲线的控制点,就可以绘制了。

- (void)drawRect:(CGRect)rect
{
CGContextRef cgContext = UIGraphicsGetCurrentContext();

CGContextMoveToPoint(cgContext, 333, 0);
CGContextAddCurveToPoint(cgContext, 333, 0, 332, 26, 330, 26);
CGContextAddCurveToPoint(cgContext, 330, 26, 299, 20, 299, 17);
CGContextAddLineToPoint(cgContext, 296, 17);
CGContextAddCurveToPoint(cgContext, 296, 17, 296, 19, 291, 19);
CGContextAddLineToPoint(cgContext, 250, 19);
CGContextAddCurveToPoint(cgContext, 250, 19, 241, 24, 238, 19);
CGContextAddCurveToPoint(cgContext, 236, 20, 234, 24, 227, 24);
CGContextAddCurveToPoint(cgContext, 220, 24, 217, 19, 216, 19);
CGContextAddCurveToPoint(cgContext, 214, 20, 211, 22, 207, 20);
CGContextAddCurveToPoint(cgContext, 207, 20, 187, 20, 182, 21);
CGContextAddLineToPoint(cgContext, 100, 45);
CGContextAddLineToPoint(cgContext, 97, 46);
CGContextAddCurveToPoint(cgContext, 97, 46, 86, 71, 64, 72);
CGContextAddCurveToPoint(cgContext, 42, 74, 26, 56, 23, 48);
CGContextAddLineToPoint(cgContext, 9, 47);
CGContextAddCurveToPoint(cgContext, 9, 47, 0, 31, 0, 0);
CGContextStrokePath(cgContext);

}


其中CGContextAddCurveToPoint函数是定义贝塞尔曲线

void CGContextAddCurveToPoint(
CGContextRef __nullable c,
CGFloat cp1x,              //第一控制点x坐标
CGFloat cp1y,              //第一控制点y坐标
CGFloat cp2x,              //第二控制点x坐标
CGFloat cp2y,              //第二控制点y坐标
CGFloat x,                 //结束点x坐标
CGFloat y                  //结束点y坐标
);


2.4 Quartz 坐标变换

图形的另一种操作就是变换,主要包括平移、缩放和旋转等形式变换。变换离不开坐标,不同的绘图系统对于坐标系的定义也有所区别。

2.4.1 坐标系

在苹果的2D图形技术是Quartz 2D和UIKit,Quartz 2D是Mac OS X和iOS环境下的2D绘图引擎。涉及内容包括:基于路径的绘图,透明度绘图,遮盖,阴影,透明层,颜色管理,防锯齿渲染,生成PDF,以及PDF元数据相关处理。在iOS中还可以通过UIKit进行图形绘制,但是Quartz 2D和UIKit坐标系不同。



如果绘制这样的图片在不同坐标系下会有什么不同呢?

- (void)drawRect:(CGRect)rect {
NSString *path = [[NSBundle mainBundle] pathForResource:@"cat" ofType:@"png"];

UIImage *img = [UIImage imageWithContentsOfFile:path];

CGImageRef image = img.CGImage;

CGContextRef context = UIGraphicsGetCurrentContext();

CGContextSaveGState(context);

CGRect touchRect = CGRectMake(0, 0, img.size.width, img.size.height);
CGContextDrawImage(context, touchRect, image);

CGContextRestoreGState(context);
}


上面的视线代码方式全部通过Quartz 2D函数实现绘制,也可以实现绘制图像的目的,其中CGContextDrawImage函数相当于UIImage类中的绘制图像方法。但是绘制出来的结果令人沮丧,图像倒过来了,这是因为Quartz 2D坐标系和UIKit坐标系不同

为了能正确的显示在UIKit坐标系下,需要进行坐标变换,需要修改代码如下:

- (void)drawRect:(CGRect)rect {

NSString *path = [[NSBundle mainBundle] pathForResource:@"cat" ofType:@"png"];

UIImage *img = [UIImage imageWithContentsOfFile:path];

CGImageRef image = img.CGImage;

CGContextRef context = UIGraphicsGetCurrentContext();

CGContextSaveGState(context);

CGContextTranslateCTM(context, 0, img.size.height);
CGContextScaleCTM(context, 1, -1);

CGRect touchRect = CGRectMake(0, 0, img.size.width, img.size.height);
CGContextDrawImage(context, touchRect, image);

CGContextRestoreGState(context);
}


2.4.2 2D图形的基本变换

在图形变换过程中需要大量使用矢量、矩阵及其运算,学习图形变换需要了解这些基本矩阵知识,关于这些知识该书中没有介绍,2D图形的基本变换包括:平移、缩放和旋转三种变换

1.平移变换

平移是一物体从一个位置到另一个位置所做的直线移动。如果要把一个位于P(x,y)的点移到新位置p’(x’,y’)时,只要在原坐标加上平移距离Tx及Ty即可,如图2-19所示。

2.缩放变换

用来改变一物体大小的变换称为缩放变换。如果要对一个多边形进行比例变换,那么可把各顶点的坐标(x,y)均乘以比例因子Sx、Sy可以相等或不等。如果比例因子数值小于1,则物体尺寸减小;大于1,则使物体放大;Sx及Sy都等于1,则物体大小形状不变。需要注意的是图2-20表示的比例变换是针对坐标原点的。



3.旋转变换

物体上的各点绕一固定点沿圆周路径转动称为旋转变换。我们可用旋转角表示旋转量的大小。一个点由位置(x,y)旋转到(x’,y’)的角度为自水平轴算起的角度,sita(打不出该符号)为旋转角,如图2-21所示。

有的图形系统海提供另外几种很有用的变换,如反射变换及错切变换等,我们重点介绍一下反射变换。反射是用来产生物体的镜像的一种变换。物体的镜像一般是相对于一个对称轴产生的,因此反射变换可以分为x轴对称变换、y轴对称变换和坐标原点的对称变换。

4.x轴对称变换

x轴的对称变换是一种特殊形式的缩放变换,其中Sx=1,Sy=-1,如果2-22所示



5.y轴对称变换

y轴的对称变换是一种特殊形式的缩放变换,其中Sx=-1,Sy=1,如图2-23所示

6.坐标原点的对称变换

关于坐标原点的对称变换是一种特殊形式的缩放变换,其中Sx=-1,Sy=-1,如图2-24所示



2.4.3 CTM变换矩阵

Quartz 2D提供了多种形式的变换,其中主要是当前变换矩阵变换(current transformation matrix,CTM)和仿射(affine)变换。CTM变换,这种变换非常简单,主要的函数有:

CGContextRotateCTM,旋转变换

CGContextScaleCTM,缩放变换

CGContextTranslateCTM,平移变换

1.平移变换

平移变换根据指定的Tx,Ty值移动坐标系统的原点。我们通过调用CGContextTranslateCTM函数来改变每个点的x,y坐标值。如图2-25右所示显示了一幅图片沿x轴移动了100个单位,沿y轴移动了50个单位。具体代码如下:

- (void)drawRect:(CGRect)rect {

NSString *path = [[NSBundle mainBundle] pathForResource:@"cat" ofType:@"png"];

UIImage *img = [UIImage imageWithContentsOfFile:path];

CGImageRef image = img.CGImage;

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);

CGContextTranslateCTM(context, 100, 50);

CGRect touchRect = CGRectMake(0, 0, img.size.width, img.size.height);
CGContextDrawImage(context, touchRect, image);

CGContextRestoreGState(context);
}




2.缩放变换

缩放操作根据指定的Sx,Sy因子来改变图像的大小,从而放大或缩小图像,Sx,Sy因子的大小决定了新的坐标系是否比原坐标系大或者小。图2-26显示了指定Sx因子为0.5,Sy因子为0.75后的缩放效果。

在上一段代码的基础上更改此句
CGContextScaleCTM(context, .5, .75);




另外,通过指定Sx因子为负数是X轴对称变换,同样可以指定Sy因子为负数是y轴对称变换。通过调用CGContextScaleCTM函数来指定Sx,Sy缩放因子。

3.旋转变换

Sx,Sy旋转变换根据指定的角度来旋转坐标。我们可以通过CGContextRotateCTM函数来指定旋转角度(以弧度为单位)。图2-27右图所示,显示了图片以原点为中心顺时针旋转45度,代码如下所示。

在上一段代码的基础上更改此句
CGContextRotateCTM(context, radians(45.));




其中,radians是我们指定的宏,用来将弧度转化为度。我们需要h文件中添加如下代码。

#definne radians(x) (x*M_PI/180)


由于旋转操作使图片的部分区域置于上下文之外,所以区域外的部分被裁减,如果旋转的弧度为负数,则图形使逆时针旋转。

CGContextRotateCTM(context, radians(-45.));


有些情况下需要组合变换,得到累加效果。

- (void)drawRect:(CGRect)rect {

NSString *path = [[NSBundle mainBundle] pathForResource:@"cat" ofType:@"png"];

UIImage *img = [UIImage imageWithContentsOfFile:path];

CGImageRef image = img.CGImage;

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);

CGContextTranslateCTM(context, 0, img.size.height);
CGContextScaleCTM(context, 1, -1);

CGRect touchRect = CGRectMake(0, 0, img.size.width, img.size.height);
CGContextDrawImage(context, touchRect, image);

CGContextRestoreGState(context);
}


其中,先进行了平移变换,再进行了缩放变换,CGContextScaleCTM(context,1,-1)缩放的结果是进行了y轴对称变换。要想达到这个效果上面的组合方式不是唯一的。

提示:当相同的绘制程序在一个UIView对象和Quartz图像上下文上进行绘制时候,需要做一个变换,使Quartz图形上下文与UIView具有相同的坐标系。要达到这一目的,需要将Quartz图形上下文的原点平移到左上角,再乘以-1对(y轴对称变换),如图2-29显示了这种转换过程。





2.4.4 仿射(affine)变换

仿射(affine)变换也是一种2D坐标变换,它可以重用变换,经过多次变换(即多次的矩阵相乘),每一种变换都可以用矩阵表示,通过多次矩阵相乘得到最后结果。仿射变换函数:

CGAffineMakeRotation 创建新的旋转矩阵

CGAffineMakeScale 创建新的缩放矩阵

CGAffineMakeTranslation 创建新的平移矩阵

CGAffineTransformRotate 旋转矩阵

CGAffineTransformScale 缩放矩阵

CGAffineTransformTranslate 平移矩阵

CGContextConcatCTM 连接到CTM变换

如图2-28右所示的效果,可以使用仿射变换,代码如下:

- (void)drawRect:(CGRect)rect {

NSString *path = [[NSBundle mainBundle] pathForResource:@"cat" ofType:@"png"];

UIImage *img = [UIImage imageWithContentsOfFile:path];

CGImageRef image = img.CGImage;

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);

// CGAffineTransformMakeTranslation创建新的平移变换矩阵
CGAffineTransform myAffine = CGAffineTransformMakeTranslation(0, img.size.height);
// 在平移变换矩阵上乘以缩放变换矩阵
myAffine = CGAffineTransformScale(myAffine, 1, -1);
// 连接到CTM矩阵病输出结果
CGContextConcatCTM(context, myAffine);

CGRect touchRect = CGRectMake(0, 0, img.size.width, img.size.height);
CGContextDrawImage(context, touchRect, image);

CGContextRestoreGState(context);
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: