iOS 绘图学习笔记 - (1)绘图的基本内容

iOS中能够用来绘图的框架一般是 UIKit , Quartz 2D(CoreGraphic), OpenGL, 三个框架的是从高阶framework -> 底层framework, 一般情况下, iOS中常用的是前两个.

UIKit(OC)/Quartz(C风格)两者代码风格不一样, 绘制同一个内切圆图, 两者代码如下:

// UIKit
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:inset cornerRadius:12];
[bezierPath stroke];

// Quartz 2D
CGContextFillEllipseInRect(context, rect);

使用的场景

自定义view

我们在自定义view的drawRect方法中可以绘制我们需要的view样式, 我们可以在该方法中使用UIKit或者Quartz中的方法来定制.

绘制image

app或者sdk中使用image资源时候,为了省时省力, 我们可能需要自己通过代码绘制一些简单的图,或者对已经存在的image资源进行小幅度的处理,然后使用.

例如有时候需要纯色资源图, 只需要几行代码就能够生成. 还有的时候需要改变图片的渲染颜色, 改变图片的大小以及获取镜像图片等等需求, 都能使用UIKit/Quartz绘制.

创建PDF

日常开发中使用比较少

使用CoreGraphic

UIKit中的绘图和处理图片的方法有限, 并且很多比较复杂的需求, 例如将图片灰度化等等.

Context

iOS的每次绘图操作都需要一个context, 这个context就比较像现实中的一张纸(Android中对应的canvase). 我们用context表示绘制以后的目的地, 里面保存着绘画中需要的所有的东西, 例如使用何种颜色绘制边框, 用什么颜色填充, 画布是否需要旋转等等.

我们在iOS绘画过程中一般需要接触两种context: 一种是bitmap context, 另外一种是PDF context. 其实还有一种CoreImage框架中的context 类型, 但是它用于图像处理而不是绘画.

bitmap context

BitmapContext是一个长方形的数组的内容, 其中数组是一个二维数组, 成员就是图形的每个像素,像素的size用来表示每个像素所代表的颜色类型所占用的空间. 一般而言, 有些图片使用3或4个bytes表示一个像素, 两者不同是是否有alpha通道(bitmap 的opaque).

一个 opaque 的bitmap会忽略图片的alpha透明属性,以此来优化存储大小. 通常透明度alpha,又表示该像素点的亮度信息.

我们通常用的灰度图, 使用1个或者2个bytes表示一个像素,其中有一个就是亮度.

PDF context

Core Image Contexts

如何在UIKit中获取Contexts

UIKit中获取image context方法非常简单, 值得注意的是,使用下面代码的方法,获取的图片的绘制比例是1:1, 即context的大小是就是像素大小:

UIGraphicsBeginImageContext(size);
// Perform drawing here
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

设备的Device scale

还有另外一个方法用来获取image context, 并且创建的图片是和不同屏幕分辨率相匹配的. Device scale表示了 逻辑空间(iOS中的point) 与 物理空间(像素pixel)之间的比例.

一个图片的scale在iOS开发中非常重要. @2x, @3x图的意义就在这里. 下面代码中的deviceScale可以填写成0.0 或者 [UIScreen mainScreen].scale, 系统会根据当前设备自动匹配, 强烈建议在开发中使用如下方法, 因为如果使用airplay等其他的方法,会很好的进行频幕分辨率的适配.

UIGraphicsBeginImageContextWithOptions(targetSize, isOpaque, deviceScale);
// Perform drawing here
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

更加底层的API方法创建context

CoreGraphic 提供了一些方法用于创建bitmap context:

// Create a color space
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
if (colorSpace == NULL) {
    NSLog(@"Error allocating color space");
    return nil; 
}
// Create the bitmap context. (Note: in new versions of 
// Xcode, you need to cast the alpha setting.) 

CGContextRef context = CGBitmapContextCreate(
            NULL, width, height,
            BITS_PER_COMPONENT, // bits = 8 per component 
            width * ARGB_COUNT, // 4 bytes for ARGB colorSpace,
            (CGBitmapInfo) kCGImageAlphaPremultipliedFirst);
            
if (context == NULL) {
    NSLog(@"Error: Context not created!");  CGColorSpaceRelease(colorSpace ); return nil;
}

// Push the context.
// (This is optional. Read on for an explanation of this.)
// UIGraphicsPushContext(context);

// Perform drawing here

// Balance the context push if used.
// UIGraphicsPopContext();

// Convert to image
CGImageRef imageRef = CGBitmapContextCreateImage(context);
UIImage *image = [UIImage imageWithCGImage:imageRef];

// Clean up CGColorSpaceRelease(colorSpace ); CGContextRelease(context); 
CFRelease(imageRef);

在context中绘制

Quartz中有很多函数让你在context中绘制, 当然前提是你持有了一个CGContextRef对象:

// Set the line width 
CGContextSetLineWidth(context, 4);

// Set the line color 
CGContextSetStrokeColorWithColor(context,
[UIColor grayColor].CGColor);

// Draw an ellipse 
CGContextStrokeEllipseInRect(context, rect);

在UIKit中生成context并绘制

UIKit中很容易创建生成bitmap context:

// 建立image context
UIGraphicBeginImageContextWithOptions(targetSize, isOpaque, 0.0);

// 获取当前的context
CGContextRef context = UIGraphicsGetCurrentContext();

//perform the drawing
CGContextSetLineWidth(context, 4);
CGContextSetStrokeColorWithColor(context, [UIColor grayColor]).cgColor;
CGContextStrokeEllipseInRect(context, rect);

//获取绘制的图片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

// 结束image context - 注意这里无需清理context这个局部变量, 因为不是我们create出来的, 在End方法里面系统会将image context对象内存回收
UIGraphicsEndImageContext();

注意, iOS系统会维护一个context栈,在UIGraphicBeginImageContextWithOptions方法调用时候, 系统就会创建一个bitmap context, 并且将该context压入栈顶, 在使用UIGraphicsGetCurrentContext时候,获取的栈顶的context, 如果我们调用UIGraphicsEndImageContext,系统会将栈顶的context移除, 此时如果我们再次调用UIGraphicsGetCurrentContext那么可能拿到一个nil对象.

当然我们也可以通过方法UIGraphicsPushContext(context)手动将某一个context压入栈顶, 通过UIGraphicsPopContext()将其出栈.这种手动的方法常常用于UIKit中的-drawRect:方法, 因为这种可以切换当前的context, 让我可以同时在不同的地方进行绘制.

在自定义UIView中, 当系统调用-drawRect:方法时, 系统会push一个context到栈顶,因此,在我们重写该方法时候, 我们可以直接调用UIGraphicsGetCurrentContext()获取栈顶的context, 而不必担心栈顶没有context:

-(void)drawRect:(CGRect)rect{
    // Perform drawing here
    // If called, UIGraphicsGetCurrentContext()
    // returns a valid context
}

UIKit中的当前 Context

前面我们注意到, CoreGraphic中的每个绘制相关方法都需要第一个参数就是context, 但是使用UIKit框架的绘制方法, 不需要我们传入context.

// Stroke an ellipse using a Bezier path
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:rect]; path.lineWidth = 4;
// UIKit的绘制方法 - 设置描边颜色时候, 无需传入context
[[UIColor grayColor] setStroke];
[path stroke];

那么在使用UIKit中的绘制方法时候, UIKit会维护graphic context的栈. 对于上面的UIKit的绘制代码. 下面是系统所做的内容:

  1. 创建CoreGraphic context(系统)
  2. 调用UIGraphicsPushContext(), 将该context入栈(系统)
  3. 使用UIKit的绘制方法 -[[UIColor grayColor] setStroke]等等(自己的代码)
  4. (可能获取当前的图片) (自己的代码)
  5. 调用UIGraphicsPopContext(), 将context出栈(系统)
  6. 清理前面创建的context的内存(系统)

绘画的模式

iOS 的绘画模式是painter's model, 除非指定, 我们后绘制的内容,总是会覆盖在之前绘制的内容之上, 并且我们在某一时候改变context的某些属性,例如strokeColor,只会影响后面绘制的内容, 并不会影响前面绘制的内容.

Context state状态

Context是有维护一个栈来管理自己的状态的, 例如设置颜色, 线宽等等,都需要设置状态, 下面有实例表示:

UIGraphicsBeginImageContext(size);
CGContextRef context = UIGraphicsGetCurrentContext();

// Set initial stroke/fill colors
[greenColor setFill]; 
[purpleColor setStroke];

// Draw the bunny 
[bunnyPath fill]; 
[bunnyPath stroke];

// Save the state 
CGContextSaveGState(context);

// Change the fill/stroke colors 
[[UIColor orangeColor] setFill]; 
[[UIColor blueColor] setStroke];

// Move then draw again 
[bunnyPath applyTransform:
CGAffineTransformMakeTranslation(50, 0)]; 
[bunnyPath fill];
[bunnyPath stroke];

// Restore the previous state 
CGContextRestoreGState(context);

// Move then draw again 
[bunnyPath applyTransform:
CGAffineTransformMakeTranslation(50, 0)]; 
[bunnyPath fill];
[bunnyPath stroke];

UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext();

一个context会保存很多类型的状态, 常见的有如下几个: Color, tramsformation matrices, Clipping, Line parameters, Flatness, Alpha levels, Text traits, Blend modes等等

Context 坐标系

UIKit的坐标系是top-left, 而在Quartz中坐标系是bottom-left. 有一点非常重要: context的坐标系是是用的哪种, 关键在于我们通过哪里获取的context,如果我们使用任何UIKit的函数获取的Context,那么它的坐标系就是top-left,如果我们使用的CGBitmapContextCreate()方法创建的Context,那么坐标系就是bottom-left

翻转context坐标系

就算UIKit和Quartz的坐标系不匹配, 我们可以通过tramsform翻转一个坐标系, 使得两者映射到设备上时保持一致.

  1. 将CGContextRef push进入UIkit的stack
  2. 沿着水平轴对称翻转context
  3. 上移context
  4. 在新的坐标系中drawing
  5. 将context pop出栈

具体代码如下:

// Flip context by supplying the size 
void FlipContextVertically(CGSize size) {
    CGContextRef context = UIGraphicsGetCurrentContext(); 
    if (context == NULL){
        NSLog(@"Error: No context to flip");
        return; 
    }
    CGAffineTransform transform = CGAffineTransformIdentity;
    // y轴翻转
    transform = CGAffineTransformScale(transform, 1.0f, -1.0f);
    // 上移动context的高度
    transform = CGAffineTransformTranslate(transform, 0.0f, -   size.height);
    // 应用到当前context
    CGContextConcatCTM(context, transform);
}

裁剪context坐标系

我们可以通过context 的clip, 将不需要的内容清除, 通常使用剪裁时候, 需要先保存当前context的状态, 因为剪切以后, 后面可以方便恢复原来的context 的状态.

  1. CGContextSaveGState(context) 保存当前context状态(可以省略)
  2. 在context中添加一个你需要clip的path.这个path是用于你clip当前context的mask, 这个mask以外的内容,就会被裁剪掉. 对于path,可以使用CGPathRef或者UIBezierPath, 然后调用对应的clip方法
  3. 做其他的drawing操作, 都是在裁剪以后的context上进行的,如果绘图操作在裁剪区域以外, 将被忽略.
  4. CGContextRestoreGState(context)(可以忽略), 然后做其他的绘制操作

具体的实例代码如下:

// Save the state 
CGContextSaveGState(context);

// Add the path and clip 
CGContextAddPath(context, path.CGPath); 
CGContextClip(context);

// Perform clipped drawing here

// Restore the state 
CGContextRestoreGState(context);

// Drawing done here is not clipped

Transfrom变换

我们设想一种场景, 需要一个绘制一串字母"ABCDEFGHIJKLMNOPQRSTUVWXYZ", 绘制成圆形, 均匀分布. 因此每个字母的基于原点的弧度就是 (2 × Pi / 26) radians, 如下实例代码能够完成:

NSString *alphabet = @"ABCDEFGHIJKLMNOPQRSTUVWXYZ"; for(inti=0;i<26;i++) {
    NSString *letter = [alphabet substringWithRange:NSMakeRange(i, 1)];
    CGSize letterSize = [letter sizeWithAttributes:@{NSFontAttributeName:font}];
    CGFloat theta = M_PI - i * (2 * M_PI / 26.0);
    CGFloat x = center.x + r * sin(theta) - letterSize.width / 2.0;     CGFloat y = center.y + r * cos(theta) - letterSize.height / 2.0;
    [letter drawAtPoint:CGPointMake(x, y) withAttributes:@{NSFontAttributeName:font}];
}

但是,所有绘制的字符都是正的,并没有完全满足需要如果需要, 实际上我们可以根据当前context transforms来完成这个需求.

Transform 状态

context中有一个状态中affine transform非常重要, 一般称为current transform matrix, 这个指定当前状态需要旋转,平移,缩放.

这里参考book上的代码

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

推荐阅读更多精彩内容