- 视图基础
- 视图是 UIView 对象,或者其子对象。
- 视图知道如何绘制自己。
- 视图可以处理事件,例如触摸(touch)。
- 视图会按照层次结构排列,位于视图层次结构顶端的是应用窗口。
- 视图层次结构
任何应用有且只有一个 UIWindow 对象。 UIWindow 对象就像是一个容器,负责包含应用中的所有的视图。应用需要在启动时创建并设置 UIWindow 对象,然后为其添加其他视图。
加入窗口的视图会成为该窗口的子视图。窗口的子视图还可以有自己的子视图,从而构成一个以 UIWindow 对象为根视图的,类似于树形结构的视图层次结构。
视图层次结构形成之后,系统会将其绘制到屏幕上,绘制过程可以分为两步:
1. 层次结构中的每个视图(包括 UIWindow 对象)分别绘制自己。视图会将自己绘制到图层( layer )上,每个 UIView 对象都有一个 layer 属性,指向一个 CALayer 类的对象
2. 所有视图的图层何曾一幅图像,绘制到屏幕上。
获取当前应用程序的 UIWindow 方法是 UIWindow * keyWindow = [UIApplication sharedApplication].keyWindow;
- 创建 UIView 子类
首先创建一个 UIView 子类。
视图及其 frame 属性
在控制器中创建一个 CGRect 结构,然后使用该结构创建一个视图对象,并将这个视图对象加入到控制器视图子视图上。
#import "ViewController.h" #import "JXHypnosisView.h" // 为创建的子类 @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 创建 CGRect 结构 CGRect rect = CGRectMake(100, 100, 100, 200); // 创建视图 JXHypnosisView * firstView = [[JXHypnosisView alloc] initWithFrame:rect]; firstView.backgroundColor = [UIColor redColor]; // 将视图添加到控制器View上 [self.view addSubview:firstView]; } @end
显示结果
CGRect 结构包含该另外两个结构: origin 和 size 。其中 origin 的类型是 CGPoint 结构,该结构包含两个 float 类型测成员。 size 的类型是 CGSize 结构,该结构也包含两个 float 类型的成员: width 和 height 。
所以我们创建的视图对象,在上图中可以看出 JXHypnosisView 对象的左上角位于父视图右侧 100点 、下方 200点 的位置。此外,因为这个 frame 结构中的 size 是(100,200),所以我们自定义 JXHypnosisView 对象的宽度是 100点 、高度是 200点 。
我们这里所说的这些值的单位是 点(points),不是 像素(pixels)。如果是像素,那么在不同的 Retina 显示屏上显示的大小是不同的。在 Retina 显示屏上,一个点是两个像素高度。(所以在跟美工沟通的时候最好让他们根据像素来做图片,并且图片的像素大小是点的两倍,或者三倍)。
每个视图对象都有一个 superview 属性。将一个视图作为子视图加入另一个视图时,会自动创建相应的反向关联。
- 在 drawRect: 方法中自定义绘图
前面我们编写了一个简单的自定义的 JXHypnosisView 对象,并且设置了他的一些基本的属性,如位置,大小,颜色等。在本节中我们将在 drawRect: 方法中编写绘图代码。
视图根据 drawRect: 方法将自己绘制到图层上。 UIView 的子类可以覆盖 drawRect: 方法完成自定义的绘图任务。例如, UIButton 的 drawRect: 方法默认会在 frame 表示的矩形区域中心画出一行浅蓝色的文字。
覆盖 drawRect: 后首先应该获取视图从 UIView 继承而来的 bounds 属性,该属性定义了一个矩形范围,表示视图的绘制区域。
视图在绘制自己时,会参考一个坐标系, bounds 表示的矩形位于自己的坐标系,而 frame 表示的矩形位于父视图的坐标系,但是两个矩形的大小是相同的。
frame 和 bounds 表示的矩形用法不同。前者用于确定与视图层次结构中其他视图的相对位置,从而将自己的图层与其他视图的图层正确组合成屏幕上的图像。而后者属性用于确定绘制区域,避免将自己绘制到图层边界之外(其视图是相对于自己而言,设置只有宽高有效)。
- 绘制圆形
接下来在 JXHypnosisView 的 drawRect 方法中添加绘图代码,画出一个尽可能大的圆形,但是不能好过视图的绘制区域。
首先,需要根据视图的 bounds 属性找到绘制预期的中心点:
#import "JXHypnosisView.h" @implementation JXHypnosisView - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根据bounds计算中心点 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; } @end
然后再比较视图的宽和高,将较小的值的一般设置为圆形的半径:
#import "JXHypnosisView.h" @implementation JXHypnosisView - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根据bounds计算中心点 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 根据视图的宽高比较中的较小的值计算圆形的半径 float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0); } @end
- UIBezierPath
UIBezierPath 是用来绘制直线或者曲线的一个类。
首先要创建一个 UIBezierPath 对象:
#import "JXHypnosisView.h" @implementation JXHypnosisView - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根据bounds计算中心点 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 根据视图的宽高比较中的较小的值计算圆形的半径 float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0); UIBezierPath * path = [[UIBezierPath alloc] init]; } @end
接下来我们定义 UIBezierPath 对象需要绘制的路径。
#import "JXHypnosisView.h" @implementation JXHypnosisView - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根据bounds计算中心点 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 根据视图的宽高比较中的较小的值计算圆形的半径 float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0); UIBezierPath * path = [[UIBezierPath alloc] init]; // 以中心点为圆心,radius的值为半径,定义一个 0 到 M_PI * 2.0 弧度的路径(整圆) [path addArcWithCenter:center radius:radius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; } @end
路径已经定义好了,但是之定义路径不会进行实际的绘制。我们还需要向 UIBezierPath 对象发送消息,绘制之前定制的路径:
#import "JXHypnosisView.h" @implementation JXHypnosisView - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根据bounds计算中心点 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 根据视图的宽高比较中的较小的值计算圆形的半径 float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0); UIBezierPath * path = [[UIBezierPath alloc] init]; // 以中心点为圆心,radius的值为半径,定义一个 0 到 M_PI * 2.0 弧度的路径(整圆) [path addArcWithCenter:center radius:radius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; // 绘制路径 [path stroke]; } @end
绘制结果:
现在改变圆形的线条的粗细和颜色。
#import "JXHypnosisView.h" @implementation JXHypnosisView - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根据bounds计算中心点 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 根据视图的宽高比较中的较小的值计算圆形的半径 float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0); UIBezierPath * path = [[UIBezierPath alloc] init]; // 以中心点为圆心,radius的值为半径,定义一个 0 到 M_PI * 2.0 弧度的路径(整圆) [path addArcWithCenter:center radius:radius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; // 设置线条宽度为 10 点 path.lineWidth = 10; // 绘制路径 [path stroke]; } @end
下面来改变绘制图形的轨迹颜色
#import "JXHypnosisView.h" @implementation JXHypnosisView - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根据bounds计算中心点 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 根据视图的宽高比较中的较小的值计算圆形的半径 float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0); UIBezierPath * path = [[UIBezierPath alloc] init]; // 以中心点为圆心,radius的值为半径,定义一个 0 到 M_PI * 2.0 弧度的路径(整圆) [path addArcWithCenter:center radius:radius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; // 设置线条宽度为 10 点 path.lineWidth = 10; // 设置绘制颜色为灰色 [[UIColor lightGrayColor] setStroke]; // 绘制路径 [path stroke]; } @end
运行结果:
这里我们可以尝试视图的 backgroundColor 属性不会受到 drawRect 中代码的影响,通常应该将重写 drawRect 方法的视图的背景色设置为透明(对应于 clearColor),这样可以让视图只显示 drawRect 方法中绘制的内容。
#import "ViewController.h" #import "JXHypnosisView.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 创建 CGRect 结构 CGRect rect = CGRectMake(100, 200, 200, 300); // 创建视图 JXHypnosisView * firstView = [[JXHypnosisView alloc] initWithFrame:rect]; firstView.backgroundColor = [UIColor redColor]; NSLog(@"%f",firstView.bounds.origin.x); // 将视图添加到控制器View上 [self.view addSubview:firstView]; }
#import "JXHypnosisView.h" @implementation JXHypnosisView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // 设置 JXHypnosisView 对象的背景颜色为透明 self.backgroundColor = [UIColor clearColor]; } return self; } - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根据bounds计算中心点 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 根据视图的宽高比较中的较小的值计算圆形的半径 float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0); UIBezierPath * path = [[UIBezierPath alloc] init]; // 以中心点为圆心,radius的值为半径,定义一个 0 到 M_PI * 2.0 弧度的路径(整圆) [path addArcWithCenter:center radius:radius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; // 设置线条宽度为 10 点 path.lineWidth = 10; // 设置绘制颜色为灰色 [[UIColor lightGrayColor] setStroke]; // 绘制路径 [path stroke]; } @end
- 绘制同心圆
在 JXHypnosisView 中绘制多个同心圆有两个方法,第一个方法是创建多个 UIBezierPath 对象,每个对象代表一个圆形;第二个方法是使用一个 UIBezierPath 对象绘制多个圆形,为每个圆形定义一个绘制路径。很明显第二种方法更好。
#import "JXHypnosisView.h" @implementation JXHypnosisView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // 设置 JXHypnosisView 对象的背景颜色为透明 self.backgroundColor = [UIColor clearColor]; } return self; } - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根据bounds计算中心点 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 根据视图的宽高比较中的较小的值计算圆形的半径 float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0); // 是最外层圆形成为视图的外接圆 float maxRadius = hypotf(bounds.size.width, bounds.size.height) / 2.0; UIBezierPath * path = [[UIBezierPath alloc] init]; // 以中心点为圆心,radius的值为半径,定义一个 0 到 M_PI * 2.0 弧度的路径(整圆) [path addArcWithCenter:center radius:radius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) { [path addArcWithCenter:center radius:currentRadius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; } // 设置线条宽度为 10 点 path.lineWidth = 10; // 设置绘制颜色为灰色 [[UIColor lightGrayColor] setStroke]; // 绘制路径 [path stroke]; } @end
运行结果:可以看到我们已经画出了一些列的同心圆,但是屏幕右边多出了一条奇怪的线条
这是因为单个 UIBezierPath 对象将多个路径(每个路径可以画出一个圆形)连接起来,形成了一个完成的路径。可以将 UIBezierPath 对象想象成一支在纸上画画的铅笔-但是当我们绘制完成一个圆形之后去绘制另外一个圆形时,铅笔并没有抬起,所以才会出现一条很奇怪的线条。
#import "JXHypnosisView.h" @implementation JXHypnosisView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // 设置 JXHypnosisView 对象的背景颜色为透明 self.backgroundColor = [UIColor clearColor]; } return self; } - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根据bounds计算中心点 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 是最外层圆形成为视图的外接圆 float maxRadius = hypotf(bounds.size.width, bounds.size.height) / 2.0; UIBezierPath * path = [[UIBezierPath alloc] init]; for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) { // 用来设置绘制起始位置 [path moveToPoint:CGPointMake(center.x + currentRadius, center.y)]; [path addArcWithCenter:center radius:currentRadius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; } // 设置线条宽度为 10 点 path.lineWidth = 10; // 设置绘制颜色为灰色 [[UIColor lightGrayColor] setStroke]; // 绘制路径 [path stroke]; } @end
运行结果:完美
- 绘制图像
创建一个 UIImage 对象: UIImage * logoImage = [UIImage imageNamed:@"train"]; ,然后在 drawRect 方法中将图像会知道视图上: [logoImage drawInRect:someRect]
#import "JXHypnosisView.h" @implementation JXHypnosisView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // 设置 JXHypnosisView 对象的背景颜色为透明 self.backgroundColor = [UIColor clearColor]; } return self; } - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根据bounds计算中心点 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 是最外层圆形成为视图的外接圆 float maxRadius = hypotf(bounds.size.width, bounds.size.height) / 2.0; UIBezierPath * path = [[UIBezierPath alloc] init]; for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) { // 用来设置绘制起始位置 [path moveToPoint:CGPointMake(center.x + currentRadius, center.y)]; [path addArcWithCenter:center radius:currentRadius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; } // 设置线条宽度为 10 点 path.lineWidth = 10; // 设置绘制颜色为灰色 [[UIColor lightGrayColor] setStroke]; // 绘制路径 [path stroke]; // 创建UIImage对象 UIImage * logoImage = [UIImage imageNamed:@"train"]; // 绘制图像 [logoImage drawInRect:bounds]; } @end
- 深入学习: Core Graphics
UIImage、UIBezierPath 和 NSString 都提供了至少一种用于在 drawRect 中绘图的方法,这些绘图的方法会在 drawRect 执行时分别将图像,图形,和文本绘制到视图的图层上。
无论是绘制 JPEG 、PDF 还是视图的图层,都是由 Core Graphics 框架完成的。 Core Graphics 是一套提供 2D 绘图功能的 C语言API,使用 C结构和 C函数模拟了一套面向对象的编程机制,并没有OC对象和方法。 Core Graphics 中最重要的“对象”是 图形上下文 ,图形上下文是 CGContextRef 的“对象”,负责存储绘画状态(例如画笔颜色和线条粗细)和绘制内容所处的内存空间。
视图的 drawRect 方法在执行之前,系统首先为视图的图层创建一个图形上下文,然后为绘画状态设置一些默认参数。 drawRect 方法开始执行时,随着图形上下文不断执行绘图操作,图层上的内容也会随之改变。 drawRect 执行完毕后,系统会将图层与其他图层一起组合成完整的图像并显示在屏幕上。
参与绘图操作的类都定义了改变绘画状态和执行绘图操作的方法,这些方法其实调用了对应的 Core Graphics 函数。例如,向 UIColor 对象发送 setCtroke 消息时,会调用 Core Graphics 中的 CGContextSetRGBSrokeColor 函数改变当前上下文中的画笔颜色。