Core Graphics 教程:Lines、Rectangle、Gradient

Core Graphics 是非常棒的iOSApI,我们可以用它来自定义一些很酷的UI,而不必依赖图片。
但是对于大部分开发者而言,它是令人畏惧的。因为它的PAI很多,有很多的东西需要去理解。
这篇文章会通过画一个tableview,来为我们一步一步的揭开Core Graphics的神秘面纱。
看起来像这样

CoreGraphics101.jpg

在这一篇教程中我们会初步的使用Core Graphics。实现像,绘制一个矩形,绘制一个渐变,还有如何处理1像素的线的问题。
在下一篇教程中我们将完成这个app的剩余部分,tableview的header,footer还有触摸事件。

Getting Started

新建一个项目选择Single View Application,输入CoolTable作为项目名称,勾选Use StoryboardsUse Automatic Reference Counting,创建项目。然后删除ViewController.hViewController.mUITableViewController来替代。

project-selection-475x320.png
project-settings-474x320.png

创建一个新类继承UITableViewController命名为CoolTableViewController

class_creation_dialog-475x320.png

选中默认的the starting viewcontroller,并删除它。从object library里面拉一个导航栏出来,

navigation-controller-480x293.png

UITableViewController的class改为你自定义的class,

identity_inspector.png

删除导航栏bar的title,

root-view-controller-e1360876556953.png

最后为cell准备一个reuse identify,使用cell,

attributes-inspector-425x320 (1).png

运行app,

BlankTableView.jpg

这是一个空白的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";
       }
}

现在继续运行

TableViewPlain.jpg

当你滑动的时候会发现第一个section header会黏在顶部

TableViewPlainHeader.jpg

因为你使用的是tableview的plain模式,你可以用grouped模式来避免这种情况的发生。

grouped_table-480x268.png

Table View Style Analyzed

我们会通过三个部分来绘制tableview:cell,header,footer。

TableViewAnalyzed.jpg

在这篇文章里我们先绘制cell,让我们仔细观察一下

TableViewCellsZoomed.jpg

我们发现了以下几点:

  • cell是渐变的从white到light gray
  • 每个cell都有白色的轮廓,除了最后一个只有一边有
  • 每个cell通过light gray颜色的线分割,除了最后一个
  • cell的边缘有锯齿状

Hellow Core Graphics!

我们的code要写在UIViewdrawRect方法里。创建一个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

HelloCoreGraphics.jpg

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);
CellsWithGradient.jpg

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方法来绘制矩形。

FuzzyLines.jpg

它看起来不错,但是仔细看会觉得有点模糊和怪异,如果放大了就能看清楚哪里不对劲。

FuzzyLines2.jpg

你希望画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

1PxSharpLines.jpg

现在我们加上正确的颜色和位置

    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

CustomCellsWhiteBorder.jpg

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

CustomCellsWithSeparator.jpg

原文地址
源码地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容