Core Graphics 是非常棒的iOSApI,我们可以用它来自定义一些很酷的UI,而不必依赖图片。
但是对于大部分开发者而言,它是令人畏惧的。因为它的PAI很多,有很多的东西需要去理解。
这篇文章会通过画一个tableview,来为我们一步一步的揭开Core Graphics的神秘面纱。
看起来像这样
在这一篇教程中我们会初步的使用Core Graphics。实现像,绘制一个矩形,绘制一个渐变,还有如何处理1像素的线的问题。
在下一篇教程中我们将完成这个app的剩余部分,tableview的header,footer还有触摸事件。
Getting Started
新建一个项目选择Single View Application,输入CoolTable作为项目名称,勾选Use Storyboards、Use Automatic Reference Counting,创建项目。然后删除ViewController.h 和 ViewController.m用UITableViewController来替代。
创建一个新类继承UITableViewController命名为CoolTableViewController
选中默认的the starting viewcontroller,并删除它。从object library里面拉一个导航栏出来,
把UITableViewController的class改为你自定义的class,
删除导航栏bar的title,
最后为cell准备一个reuse identify,使用cell,
运行app,
这是一个空白的tableview,让我们添加一些数据。选中CoolTableViewController.m 文件,添加如下code
@interface CoolTableViewController ()
@property (copy) NSMutableArray *thingsToLearn;
@property (copy) NSMutableArray *thingsLearned;
@end
这两个数组里面的数据源是填充tableview的两个section的,注意这两个数组是在私有的interface 里面声明的因为它不需要让外界知道。
继续加入下列code
- (void)viewDidLoad{
[super viewDidLoad];
self.title = @"Core Graphics 101";
self.thingsToLearn = [@[@"Drawing Rects", @"Drawing Gradients", @"Drawing Arcs"] mutableCopy];
self.thingsLearned = [@[@"Table Views", @"UIKit", @"Objective-C"] mutableCopy];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
// Return the number of sections.
return 2;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
if (section == 0) {
return self.thingsToLearn.count;
}
else
{
return self.thingsLearned.count;
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString * CellIdentifier = @"Cell";
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; NSString entry;
if (indexPath.section == 0) {
entry = self.thingsToLearn[indexPath.row];
}
else
{
entry = self.thingsLearned[indexPath.row];
}
cell.textLabel.text = entry;
return cell;
}
-(NSString *) tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
if (section == 0) {
return @"Things We'll Learn";
}
else
{
return @"Things Already Covered";
}
}
现在继续运行
当你滑动的时候会发现第一个section header会黏在顶部
因为你使用的是tableview的plain模式,你可以用grouped模式来避免这种情况的发生。
Table View Style Analyzed
我们会通过三个部分来绘制tableview:cell,header,footer。
在这篇文章里我们先绘制cell,让我们仔细观察一下
我们发现了以下几点:
- cell是渐变的从white到light gray
- 每个cell都有白色的轮廓,除了最后一个只有一边有
- 每个cell通过light gray颜色的线分割,除了最后一个
- cell的边缘有锯齿状
Hellow Core Graphics!
我们的code要写在UIView的drawRect方法里。创建一个view命名为CustomCellBackground,然后切换到CustomCellBackground.m添加code
-(void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
UIColor *redColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0];
CGContextSetFillColorWithColor(context, redColor.CGColor);
CGContextFillRect(context, self.bounds);
}
在第一行我们调用UIGraphicsGetCurrentContext()方法获得一个Core Graphics Context
在下面的方法中会用到它。
我们可以把context看做是一个画布‘canvas’,我们可以在上面绘制。在这种情况下‘canvas’是view,还有其他的画布,例如offscreen buffer,它可以变成一个图片,在将来的某个时候。
关于context的第一个有趣的东西是stateful,当处于stateful意味着我们可以改变一些东西,像填充颜色。这个填充的颜色将会被保留下来用作填充颜色,除非你在后面把它改为不同的值。
在第三行使用了CGContextSetFillColorWithColor这个方法,来把填充色设置为red。你可以在任何时候使用这个方法来填充图形。
你可能会注意到,你不能直接调用UIColor,你必须用CGColorRef这个类来替代,幸运的是它们之间的转化非常简单。
最后你调用一个方法来填充矩形,你需要传入矩形的bounds。
现在你已经有了一个red view,你将会把它设为cell的background view。
在CoolTableViewController.m的上面导入
#import "CustomCellBackground.h"
,然后修改tableView:cellForRowAtIndexPath
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString * CellIdentifier = @"Cell";
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
NSString * entry;
// START NEW
if (![cell.backgroundView isKindOfClass:[CustomCellBackground class]]) {
cell.backgroundView = [[CustomCellBackground alloc] init];
}
if (![cell.selectedBackgroundView isKindOfClass:[CustomCellBackground class]]) {
cell.selectedBackgroundView = [[CustomCellBackground alloc] init];
}
// END NEW
if (indexPath.section == 0) {
entry = self.thingsToLearn[indexPath.row];
} else {
entry = self.thingsLearned[indexPath.row];
}
cell.textLabel.text = entry;
cell.textLabel.backgroundColor = [UIColor clearColor]; // NEW
return cell;
}
run
Drawing Gradients
现在我们将会在项目中绘制许多渐变,把你的渐变code放在helper类里面方便以后在不同的项目中使用。
新建一个NSObject的子类,命名为Common删除Common.h里面的所有内容
添加如下code
#import <Foundation/Foundation.h>
void drawLinerGradient(CGContextRef context,CGRect rect, CGColorRef startColor, CGColorRef endColor);
你不是正真的创建了一个类,因为你不需要任何状态,只需要一个全局的方法。切换到Common.m添加code
#import "Common.h"
void drawLinearGradient(CGContextRef context, CGRect rect, CGColorRef startColor, CGColorRef endColor)
{
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat locations[] = {0.0,1.0};
NSArray *colors = @[(__bridge id)startColor,(__bridge id)endColor];
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)colors, locations);
// More coming...
}
这里有很多的方法。
第一件事,你需要有一个color space来绘制渐变color,通过color space你可以做很多事情。大部分时候你只需要用到RGB类型的color space,所以你只需要使用CGColorSpaceCreateDeviceRGB方法来获得你需要的引用(RGB)。
设置一个数组,在渐变范围你每种颜色的位置。0意味着开始渐变,1意味着渐变结束。你只需要两个颜色,一个用来开始渐变,一个用来结束渐变,所以你只要传入0和1。
注意,如果你想要的话你可以设置更多的渐变颜色,你要设置每种颜色开始渐变的位置。用这个方法可以实现很炫的效果哦。
想了解更多关于bridge和memory management,请看这篇教程Automatic Reference Counting.
这样你就用CGGradientCreateWithColors创建了一个渐变,传入了color space、color array、locations(颜色的位置)。
现在你有了一个渐变引用,但是不是一个真正的渐变图像。现在把下面code添加到More coming的注释下面
CGPoint startPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect));
CGPoint endPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
CGContextSaveGState(context);
CGContextAddRect(context, rect);
CGContextClip(context);
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);
CGContextRestoreGState(context);
CGGradientRelease(gradient);
CGColorSpaceRelease(colorSpace);
第一件事是计算开始和结束的点,剩下的code是帮你在rectage里面绘制渐变。主要的方法是CGContextDrawLinearGradient。这个方法很奇怪,因为它用渐变填补了画布的整个区域,也就是说,它没办法填补某个区域。Clip是Core Graphics是一个很棒的特点,你可以用它来绘制任意的图形,你要做的仅仅是把图形添加到context里面。和一起不同的是,你只需要调用CGContextClip,这样所有的绘制内容就会限制在该区域。
所以这里你添加了一个矩形到context里面,裁剪它,然后调用CGContextDrawLinearGradient传入你之前准备好的所有变量。
CGContextSaveCGState/CGContextRestoreCGState这个方法做了什么呢?记住Core Graphics有一种状态机制。只要你设置了它的状态,它就会一直保持,直到你去改变它。这里就用到了这两个方法,保存你当前context的设置到stack中。将来你想要恢复state的时候,就从stack中pop出来。
最后一件事,你需要释放memory,通过调用CGGradientRelease方法来释放CGGradientCreateWithColors方法创建的对象。
回到CustomCellBackground.m,导入#import "Common.h"
,替代drawRect方法里的code
CGContextRef context = UIGraphicsGetCurrentContext();
UIColor * whiteColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
UIColor * lightGrayColor = [UIColor colorWithRed:230.0/255.0 green:230.0/255.0 blue:230.0/255.0 alpha:1.0];
CGRect paperRect = self.bounds;
drawLinearGradient(context, paperRect, whiteColor.CGColor, lightGrayColor.CGColor);
Stroking Paths
我们要在cell四周绘制一个白色的矩形,并在cell之间绘制灰色的分割线。我们已经填充了一个矩形,划线也是很简单的。修改CustomCellBackground.m的drawRect:方法
CGContextRef context = UIGraphicsGetCurrentContext();
UIColor * whiteColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
UIColor * lightGrayColor = [UIColor colorWithRed:230.0/255.0 green:230.0/255.0 blue:230.0/255.0 alpha:1.0];
UIColor *redColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0];
CGRect paperRect = self.bounds;
drawLinearGradient(context, paperRect, whiteColor.CGColor, lightGrayColor.CGColor);
CGRect storeRect = CGRectInset(paperRect, 5.0, 5.0);
CGContextSetStrokeColorWithColor(context, redColor.CGColor);
CGContextSetLineWidth(context, 1.0);
CGContextStrokeRect(context, storeRect);
为了让这些改变容易看出来,我们在cell的中间画了一个红色的矩形。CGRectInset这个方法是返回一个矩形,该矩形的rect是原参数矩形的基础上,上下都减少了Y,左右都减了X。然后返回一个新的矩形给你。设置线宽为1point(在retain屏幕上是2pixels,非retain屏是1pixel),颜色为红色。调用CGContextStrokeRect方法来绘制矩形。
它看起来不错,但是仔细看会觉得有点模糊和怪异,如果放大了就能看清楚哪里不对劲。
你希望画1point的线,但是你可以看到像素重合了,那怎么办呢?
1 Point Lines and Pixel Boundaries
这件事证明了,用Core Graphics描一个路径,描边是以路径为中间线。
我们希望填充矩形的路径边缘,当我们沿着边缘画1pixel,一半的线(0.5pixel)在矩形里面,一半的线在矩形的外面。
因为没有办法画0.5pixel的线,所以Core Graphics用锯齿来替代。
但是我们不想要锯齿,我们需要的是1pixel的线,有下面几种办法来解决:
- 裁剪掉不想要的像素
- 使锯齿无效,修改矩形的边缘,确保达到你想要的效果
- 修改绘制路径,把0.5pixel的影响考虑进去
打开Common.h文件,添加下列方法 CGRect rectFor1PxStroke(CGRect rect);
Common.m里面
CGRect rectFor1PxStroke(CGRect rect)
{
return CGRectMake(rect.origin.x + 0.5, rect.origin.y + 0.5, rect.size.width - 1, rect.size.height - 1);
}
路径(是描边的中线)向上移了1pixel,向右移了1pixel
回到CustomCellBackground.m用
CGRect strokeRect = rectFor1PxStroke(CGRectInset(paperRect, 5.0, 5.0));
替代以前的code,run
现在我们加上正确的颜色和位置
CGContextRef context = UIGraphicsGetCurrentContext();
UIColor * whiteColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
UIColor * lightGrayColor = [UIColor colorWithRed:230.0/255.0 green:230.0/255.0 blue:230.0/255.0 alpha:1.0];
// UIColor *redColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0];
CGRect paperRect = self.bounds;
drawLinearGradient(context, paperRect, whiteColor.CGColor, lightGrayColor.CGColor);
// CGRect storeRect = CGRectInset(paperRect, 5.0, 5.0);
// CGRect storeRect = rectFor1PxStroke(CGRectInset(paperRect, 5.0, 5.0));
CGRect stroRect = paperRect;
stroRect.size.height -= 1;
stroRect = rectFor1PxStroke(stroRect);
CGContextSetStrokeColorWithColor(context, whiteColor.CGColor);
CGContextSetLineWidth(context, 1.0);
CGContextStrokeRect(context, stroRect);
这里我们减少一个高度来做分割,并把描边换成白色,这样在cell之间就有一个细微的白色,run
Drawing Lines
因为你已经在项目里面花了不少的线,我们要把它抽出来。添加到Common.h类里面
void draw1PxStroke(CGContextRef context, CGPoint startPoint, CGPoint endPoint, CGColorRef color);
Common.m里面
void draw1PxStroke(CGContextRef context, CGPoint startPoint, CGPoint endPoint,CGColorRef color)
{
CGContextSaveGState(context);
CGContextSetLineCap(context, kCGLineCapSquare);
CGContextSetStrokeColorWithColor(context, color);
CGContextSetLineWidth(context, 1.0);
CGContextMoveToPoint(context, startPoint.x + 0.5, startPoint.y + 0.5);
CGContextAddLineToPoint(context, endPoint.x + 0.5, endPoint.y + 0.5);
CGContextStrokePath(context);
CGContextRestoreGState(context);
}
在方法的开始,我们使用了save/restore,这样我们在画线的时候就不会对画布周围造成影响。
我们的线以cap的模式结束。这样可以在一定程度上达到抗锯齿的效果。
把点移动到A,画A到B的线。
改变CustomCellBackground.m里的code
CGContextRef context = UIGraphicsGetCurrentContext();
UIColor * whiteColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
UIColor * lightGrayColor = [UIColor colorWithRed:230.0/255.0 green:230.0/255.0 blue:230.0/255.0 alpha:1.0];
UIColor * separatorColor = [UIColor colorWithRed:208.0/255.0 green:208.0/255.0 blue:208.0/255.0 alpha:1.0];
// UIColor *redColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0];
CGRect paperRect = self.bounds;
drawLinearGradient(context, paperRect, whiteColor.CGColor, lightGrayColor.CGColor);
// CGRect storeRect = CGRectInset(paperRect, 5.0, 5.0);
// CGRect storeRect = rectFor1PxStroke(CGRectInset(paperRect, 5.0, 5.0));
CGRect stroRect = paperRect;
stroRect.size.height -= 1;
stroRect = rectFor1PxStroke(stroRect);
CGContextSetStrokeColorWithColor(context, whiteColor.CGColor);
CGContextSetLineWidth(context, 1.0);
CGContextStrokeRect(context, stroRect);
CGPoint startPoint = CGPointMake(paperRect.origin.x, paperRect.origin.y + paperRect.size.height - 1);
CGPoint endPoint = CGPointMake(paperRect.origin.x + paperRect.size.width - 1, paperRect.origin.y + paperRect.size.height - 1);
draw1PxStroke(context, startPoint, endPoint, separatorColor.CGColor);
Run