【IOS 开发学习总结-OC-64】Quartz 2D绘图(上)——Quartz 2D绘图基础+点线模式
Quartz 2D绘图的核心API是CGContextRef,该API专门用于绘制各种图形。
Quartz 2D绘图基础:CGContextRef
使用Quartz 2D绘图的关键步骤有两步:
1. 获取CGContextRef;
2. 调用CGContextRef的方法进行绘图。
不同场景下获取CGContextRef的方式各不相同,下面介绍iOS开发中最常见的场景下如何获取CGContextRef。
- 自定义UIView时获取CGContextRef
开发自定义UIView的方法是,开发一个继承UIView的子类,并重写该UIView的drawRect:方法,当该UIView每次显示出来时,或该UIView的内容需要更新时,系统都会自动调用该UIView的drawRect:方法。在调用drawRect:方法之前,系统会自动配置绘图环境,因此,程序只要通过如下函数即可获取CGContextRef绘图API:
CGContextRef ctx = UIGraphicsGetCurrentContext();
获取CGContextRef之后,即可进行绘图。
需要指出的是,重写UIView的drawRect:方法绘图时,它的绘图API的坐标原点位于该控件的左上角,横向为X轴,X坐标越大,位置越向右;纵向为Y轴,Y坐标越大,位置越向下。
- 创建位图时获取CGContextRef
如果需要在创建位图时获取CGContextRef,那么程序需要先调用UIGraphicsBeginImageContext()函数来创建内存中的图片。然后才能调用UIGraphicsGetCurrentContext()获取绘图的CGContextRef。例如如下代码。
// 创建内存中的图片
UIGraphicsBeginImageContext(CGSizeMake(320, 480));
// 获取向内存中图片执行绘图的CGContextRef
CGContextRef ctx = UIGraphicsGetCurrentContext();
Quartz 2D绘图的核心API是CGContextRef,该API并不是一个对象。由于Quartz 2D本身并不是面向对象的,它是面向过程的API,Quartz 2D提供了大量函数来完成绘图。
表12.2中的大部分方法都涉及使用路径,路径由另一个API:CGPathRef来代表。CGPathRef代表任意多条直线或曲线连接而成的任意图形,当CGContextRef根据CGPathRef绘制时,它可以绘制出任意的形状。
此处先用最简单的方法来绘制直线、矩形、椭圆等几何形状。
在绘图之前,还需要对绘图的颜色、线条粗细等属性进行设置,Quartz 2D提供了如表12.3所示的函数来设置绘图信息。
表12.3 设置绘图属性的相关函数
绘制几何图形
绘制几何图形
从表12.2不难看出,Quartz 2D只提供了方法来绘制线段、矩形和椭圆,如果需要绘制更复杂的图形,则需要借助路径。下面的程序先使用这些方法来绘制几个简单的图形。
新建一个Single View Application,该应用包含应用程序委托类、视图控制器类和配套的S
Storyboard界面设计文件,本例无须修改应用程序委托和视图控制器类,只需要将Storyboard界面文件中最大的UIView控件类改为使用FKGeometryView自定义类即可。
FKGeometryView是由UIView派生出来的一个子类,我们将会重写drawRect:方法进行绘图,该控件类的实现部分如下。
程序代码:
@implementation FKGeometryView
// 重写该方法进行绘图
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext(); // 获取绘图上下文
CGContextSetLineWidth(ctx, 16); // 设置线宽
CGContextSetRGBStrokeColor(ctx, 0 , 1, 0 , 1);
// ----------下面绘制3个线段测试端点形状-----------
// 定义4个点,绘制线段
const CGPoint points1[] = {CGPointMake(10 , 20), CGPointMake(100 , 20)
,CGPointMake(100 , 20) , CGPointMake(20, 50)};
CGContextStrokeLineSegments(ctx ,points1 , 4); // 绘制线段(默认不绘制端点)
CGContextSetLineCap(ctx, kCGLineCapSquare); // 设置线段的端点形状:方形端点
const CGPoint points2[] = {CGPointMake(110 , 20), CGPointMake(200 , 20)
,CGPointMake(200 , 20) , CGPointMake(120, 50)}; // 定义4个点,绘制线段
CGContextStrokeLineSegments(ctx ,points2 , 4); // 绘制线段
CGContextSetLineCap(ctx, kCGLineCapRound); // 设置线段的端点形状:圆形端点
const CGPoint points3[] = {CGPointMake(210 , 20), CGPointMake(300 , 20)
,CGPointMake(300 , 20) , CGPointMake(220, 50)}; // 定义4个点,绘制线段
CGContextStrokeLineSegments(ctx ,points3 , 4); // 绘制线段
// ----------下面绘制3个线段测试点线模式-----------
CGContextSetLineCap(ctx, kCGLineCapButt); // 设置线段的端点形状
CGContextSetLineWidth(ctx, 10); // 设置线宽
CGFloat patterns1[] = {6 , 10};
CGContextSetLineDash(ctx , 0 , patterns1 , 1); // 设置点线模式:实线宽6,间距宽10
const CGPoint points4[] = {CGPointMake(40 , 65), CGPointMake(280 , 65)}; // 定义两个点,绘制线段
CGContextStrokeLineSegments(ctx ,points4 , 2); // 绘制线段
// 设置点线模式:实线宽6,间距宽10,但第1个实线宽为3
CGContextSetLineDash(ctx , 3 , patterns1 , 1);
// 定义两个点,绘制线段
const CGPoint points5[] = {CGPointMake(40 , 85), CGPointMake(280 , 85)};
CGContextStrokeLineSegments(ctx ,points5 , 2); // 绘制线段
CGFloat patterns2[] = {5,1,4,1,3,1,2,1,1,1,1,2,1,3,1,4,1,5};
CGContextSetLineDash(ctx , 0 , patterns2 , 18); // 设置点线模式
const CGPoint points6[] = {CGPointMake(40 , 105), CGPointMake(280 , 105)};
CGContextStrokeLineSegments(ctx ,points6 , 2); // 绘制线段
// ---------下面填充矩形---------
// 设置线条颜色
CGContextSetStrokeColorWithColor(ctx, [UIColor blueColor].CGColor);
CGContextSetLineWidth(ctx, 14); // 设置线条宽度
CGContextSetFillColorWithColor(ctx, [UIColor redColor].CGColor); // 设置填充颜色
CGContextFillRect(ctx , CGRectMake(30 , 120 , 120 , 60)); // 填充一个矩形
// 设置填充颜色
CGContextSetFillColorWithColor(ctx, [UIColor yellowColor].CGColor);
CGContextFillRect(ctx, CGRectMake(80 , 160 , 120 , 60)); // 填充一个矩形
// ---------下面绘制矩形边框---------
CGContextSetLineDash(ctx, 0, 0, 0); // 取消设置点线模式
CGContextStrokeRect(ctx, CGRectMake(30 , 230 , 120 , 60)); // 绘制一个矩形边框
// 设置线条颜色
CGContextSetStrokeColorWithColor(ctx, [UIColor purpleColor].CGColor);
CGContextSetLineJoin(ctx, kCGLineJoinRound); // 设置线条连接点的形状
CGContextStrokeRect(ctx , CGRectMake(80 , 260 , 120 , 60)); // 绘制一个矩形边框
CGContextSetRGBStrokeColor(ctx, 1.0, 0, 1.0 , 1.0); // 设置线条颜色
CGContextSetLineJoin(ctx, kCGLineJoinBevel); // 设置线条连接点的形状
CGContextStrokeRect(ctx , CGRectMake(130 , 290 , 120 , 60)); // 绘制一个矩形边框
CGContextSetRGBStrokeColor(ctx, 0, 1 , 1 , 1); // 设置线条颜色
// ---------下面绘制和填充一个椭圆---------
// 绘制一个椭圆
CGContextStrokeEllipseInRect(ctx , CGRectMake(30 , 380 , 120 , 60));
CGContextSetRGBFillColor(ctx, 1, 0 , 1 , 1); // 设置填充颜色
// 填充一个椭圆
CGContextFillEllipseInRect(ctx , CGRectMake(180 , 380 , 120 , 60));
}
@end
程序中的前5行代码依次绘制了三组线段,分别测试不同的线条端点,其中第一组线段使用默认的端点风格:butt风格的端点;第二组线条使用方形端点风格:square风格的端点;第三组线条使用圆形端点风格:round风格的端点。
接下来的6行粗体字代码分别使用不同的点线模式绘制了三条横线。Quartz 2D支持功能丰富的点线模式,下一节会详细介绍Quartz 2D的点线模式。
接着的两行粗体字代码使用不同的颜色分别填充了两个矩形。
再接下来的6行粗体字代码绘制了三个矩形边框,第一个矩形边框使用了默认的连接点风格:meter风格;第二个矩形边框使用了round连接点风格;第三个矩形边框使用了bevel连接点风格。
最后的两行粗体字代码分别绘制了一个椭圆边框和填充了一个椭圆区域。
编译、运行该程序,将会看到如图12.2所示的效果。
点线模式(1)
使用Quartz 2D绘制线段或边框时,默认总是使用实线。如果希望使用点线进行绘制,可调用CGContextRef的CGContextSetLineDash(CGContextRef c,CGFloat phase, const CGFloat lengths[],size_t count)函数进行设置,该函数的第3个参数是点线模式的关键,该参数是一个CGFloat型数组(第4个参数通常用于指定该数组的长度),每个CGFloat值依次控制点线的实线长度、间距。比如该参数如下。
{2,3}:代表长为2的实线、距离为3的间距、长为2的实线、距离为3的间距……这种点线模式。
{2,3,1}:代表长为2的实线、距离为3的间距、长为1的实线、距离为2的间距、长为3的实线、距离为1的间距……这种点线模式。
{5,3,1,2}:代表长为5的实线、距离为3的间距、长为1的实线、距离为2的间距、长为5的实线、距离为3的间距、长为1的实线、距离为2的间距……这种点线模式。
通过第2个参数依次指定点线模式的实线长度、间距的重复模式,这样可以定义出任意形状的点线。
该方法的第2个参数用于指定点线的相位,该参数将会与第3个参数协同起作用,比如如下参数参数组合。
phase=1, lengths={2,3}:代表长为2的实线、距离为3的间距、长为2的实线、距离为3的间距。但开始绘制起点时只绘制长度为1的实线,因为phase为1就是控制该点线”移过”1个点。
phase=3, {5,3,1,2}:代表长为5的实线、距离为3的间距、长为1的实线、距离为2的间距、长为5的实线、距离为3的间距、长为1的实线、距离为2的间距。但开始绘制时只绘制长度为2的实线,因为phase为3就是控制该点线”移过”3个点。
下面介绍一个示例让读者对Quartz 2D的点线模式有更直观、深入的理解。首先创建一个Single View Application,该应用包含一个应用程序委托类、一个视图控制器类以及配套的Storyboard界面设计文件。为了通过该界面控制点线模式,在该界面上放置一个UISlider来控制phase参数,并添加一个按钮用于重设phase参数,还放置一个UIPickerView来控制lengths参数,该应用的设计界面如图12.3所示。
为了在程序中访问这些UI控件,将程序中的UIScrolllView绑定到控制器的scrollView IBOutlet属性,将UIPickerView绑定到控制器的picker IBOutlet属性,将UISlider绑定到phase IBOutlet属性,并为界面上按钮的Touch Up Inside事件绑定reset IBAction事件处理方法。
该控制器类的实现部分代码如下。
程序清单:
@implementation FKViewController
typedef struct {
CGFloat pattern[5];
size_t count;
} Pattern;
// 初始化多个点线模式
static Pattern patterns[] = {
{{10.0, 10.0}, 2},
{{10.0, 20.0, 10.0}, 3},
{{10.0, 20.0, 30.0}, 3},
{{10.0, 20.0, 10.0, 30.0}, 4},
{{10.0, 10.0, 20.0, 20.0}, 4},
{{10.0, 10.0, 20.0, 30.0, 50.0}, 5},
};
static NSInteger patternCount = sizeof(patterns)
/ sizeof(patterns[0]);
FKDashLineView* dashView;
-(void)viewDidLoad
{
[super viewDidLoad];
// 创建FKDashLineView自定义控件
dashView = [[FKDashLineView alloc] initWithFrame:self.scrollView.bounds];
[self.scrollView addSubview: dashView]; // 将FKDashLineView控件添加到scrollView中
[dashView setDashPattern:patterns[0].pattern
count:patterns[0].count];
// 为UIPickerView设置dataSource、delegate属性
selfself.picker.dataSource = self;
selfself.picker.delegate = self;
// 选中UIPickerView的第一行
[self.picker selectRow:0 inComponent:0 animated:NO];
// 为UIPickerView的Value Changed事件绑定事件监听器
[self.phase addTarget:self action:@selector(dashPhase)
forControlEvents:UIControlEventValueChanged];
}
-(void)dashPhase
{
// 将dashView的dahsPhase设置与UIPickerView的值相同
dashView.dashPhase = self.phase.value;
}
-(IBAction)reset
{
dashView.dashPhase = 0.0; // 将dashView的dahsPhase设为0
self.phase.value = 0.0; // 将界面上UIPickerView控件的值也设为0
}
// 该方法的返回值控制UIPickerView只包含一列
-(NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
return 1;
}
// 该方法的返回值控制UIPickerView包含多少个列表项
-(NSInteger)pickerView:(UIPickerView *)pickerView
numberOfRowsInComponent:(NSInteger)component
{
return patternCount;
}
// 该方法的返回值决定每个列表项所显示的文本
-(NSString *)pickerView:(UIPickerView *)pickerView titleForRow:
(NSInteger)row forComponent:(NSInteger)component;
{
Pattern p = patterns[row]; // 获取patterns数组的第row个元素
// 将第row个patterns数组元素的pattern成员所包含的count个值拼接起来
// 作为第row个列表项所显示的文本
NSMutableString *title = [NSMutableString
stringWithFormat:@"%.0f", p.pattern[0]];
for(size_t i = 1; i < p.count; ++i)
{
[title appendFormat:@"-%.0f", p.pattern[i]];
}
return title;
}
// 当用户选择UIPickView的指定列表项时,程序设置dashView的点线模式
-(void)pickerView:(UIPickerView *)pickerView didSelectRow:
(NSInteger)row inComponent:(NSInteger)component
{
[dashView setDashPattern:patterns[row].pattern
count:patterns[row].count];
}
@end
点线模式(2)
上面的控制器中用到了一个自定义控件类:FKDashLineView,当用户通过UISlider调整phase或通过按钮重设phase时,程序都会修改FKDashLineView的dashPhase属性,从而改变该自定义控件的绘制外观;当用户通过UIPickerView选择不同的列表项时,程序会调用FKDashLineView的setDashPattern:count:方法。
下面是FKDashLineView自定义类的接口代码。
程序清单:codes/12/12.2/DashLineTest/DashLineTest/FKDashLineView.h
@interface FKDashLineView : UIView
{
CGFloat dashPattern[10];
size_t dashCount;
}
@property(nonatomic, assign) CGFloat dashPhase;
-(void)setDashPattern:(CGFloat*)pattern count:(size_t)count;
@end
FKDashLineView的实现代码将会重写drawRect:方法来绘制该控件,绘制该控件时将会根据不同的dashPhase参数和dashPattern参数来选择使用不同的点线模式。下面是该自定义控件类的实现代码。
程序清单:codes/12/12.2/DashLineTest/DashLineTest/FKDashLineView.m
@implementation FKDashLineView
@synthesize dashPhase;
-(id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if(self != nil)
{
self.opaque = YES;
self.backgroundColor = [UIColor blackColor];
// 设置每次清空上一次绘制的内容
self.clearsContextBeforeDrawing = YES;
dashCount = 0;
dashPhase = 0.0;
}
return self;
}
-(void)setDashPhase:(CGFloat)phase
{
// 如果新传入的phase参数与原有dashPash参数不相等
if(phase != dashPhase)
{
dashPhase = phase; // 对dashPhase赋值
[self setNeedsDisplay]; // 通知该控件重绘自己
}
}
-(void)setDashPattern:(CGFloat *)pattern count:(size_t)count
{
// 如果count与dashCount不相等,或者dashPattern数组与pattern数组不相等
if((count != dashCount) || (memcmp(dashPattern, pattern
, sizeof(CGFloat) * count) != 0))
{
// 将pattern数组的值复制到dashPattern数组
memcpy(dashPattern, pattern, sizeof(CGFloat) * count);
dashCount = count; // 对dashCount赋值
[self setNeedsDisplay]; // 通知该控件重绘自己
}
}
// 重写该方法,绘制该控件
-(void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext(); // 获取绘图上下文
CGContextSetRGBStrokeColor(ctx, 1.0, 0.0, 1.0, 1.0); // 设置线条颜色
CGContextSetLineWidth(ctx, 2.0); // 设置线宽
// 设置点线模式
CGContextSetLineDash(ctx, dashPhase, dashPattern, dashCount);
CGPoint line1[] = {CGPointMake(10.0, 20.0), CGPointMake(310.0, 20.0)};
CGContextStrokeLineSegments(ctx, line1, 2); // 绘制一条线段
CGPoint line2[] = {CGPointMake(160.0, 130.0), CGPointMake(160.0, 130.0)};
CGContextStrokeLineSegments(ctx, line2, 2); // 绘制一条线段
CGContextStrokeRect(ctx, CGRectMake(10.0, 30.0, 100.0, 100.0)); // 绘制一个矩形
CGContextStrokeEllipseInRect(ctx, CGRectMake(210.0, 30.0, 100.0, 100.0)); // 绘制一个椭圆
}
@end
该程序的关键在于粗体字代码,这行粗体代码设置了绘制图形所用的点线模式,该点线模式的dashPhase、dashPattern都来自该控件的属性。这些属性会随着用户操作界面上的UISlider、UIPickerView改变,这样就可以使用不同的点线模式来绘制该UI控件上的线条了。
编译、运行该程序,可以看到如图12.4所示的结果。
当用户选择UIPickerView的不同列表项目时,可以看到界面上点线模式的实线长度、间距会随选择而动态改变;如果用户拖动界面上的UIPickerView,将会看到点线模式的phase参数不断改变,形成类似动画的效果。这是因为程序采用不同的phase绘制点线时,就会形成线条在流动的错觉。