Quartz 2D自定义UI控件

(一)什么是Quartz 2D?

  • Quartz 2D是一个二维绘图引擎,支持iOS和Mac OS系统。

(二)Quartz 2D可以做的事情?

  1. 绘制图形:线条、三角形、矩形、圆形、弧形等
    • 手势解锁
    • 绘制图表,折线图、柱状图、饼状图
  2. 绘制文字
    • 涂鸦
  3. 绘制和生成图片
    • 涂鸦
  4. 读取和生成PDF
  5. 截图和裁剪
    • 头像裁剪
  6. 自定义UI控件

(三)Quartz 2D在iOS开发中的价值

在开发iOS程序时,iOS提供了UIKit框架让我们可以利用UIKit框架中的各种各样的UI控件去搭建美观的UI界面。比如:UILabel、UIImageView、UIButton。利用这些UI控件我们可以实现一些常见的界面。但是有些时候,有些UI界面极其复杂,用普通的UI控件无法实现,这是可以利用Quartz 2D技术画出我们需要的复杂结构的UI控件。
所以Quartz 2D的最大的价值就是自定义实现我们需要的但是系统没有提供的UI控件

(四)图形上下文(理解成画板)

在Quartz 2D中的重要概念之一就是图形上下文,我们可以理解成画画的画板
图形上下文是CGContextRef类型

(五)图形上下文的作用

  1. 保存绘图信息、绘图状态
  2. 决定绘制的输出目标(绘制到什么地方去?)
    • 输出目标可以是PDF文件、Bitmap、显示器的窗口
  3. 相同的绘图序列,指定不同的Graphics Context,就可将相同的图像绘制到不同的目标上

(六)Graphics Context的类型

  • Bitmap Graphics Context
  • PDF Graphics Context
  • Window Graphics Context
  • Layer Graphics Context
  • Printer Graphics Context

(七)自定义UIView

利用Quartz 2D绘制东西到UIView上需要两个前提条件:

  1. 需要一个图形上下文。(保存绘制信息、决定绘到什么地方去)
  2. 该图形上下文跟UIView关联,如此才能将内容绘制到UIIView上去

实现步骤:

  1. 自定义View继承自UIView
  2. 实现系统的drawRect方法
  3. drawRect方法中
    3.1. 获取跟当前View相关联的图形上下文
    3.2. 绘制相应的图形内容
    3.3. 利用图形上下文将绘制的所有内容渲染显示到View上面

(八)drawRect

为什么要实现drawRect方法才能绘图到View上?

  • 因为在drawRect方法中才能取得跟View相关联的图形上下文
  • 我们需要通过在上下文中绘制内容,然后把上下文中的内容渲染到viewlayer

drawRect方法在什么时候调用?

  • view第一次显示到屏幕上时(被添加到UIWindow上时调用)
  • 调用viewsetNeedsDisplay或者setNeedsDisplayRect时调用

(九)Quartz 2D绘图代码的步骤

1. 获得图形上下文
CGContextRef ctx = UIGraphicsGetCurrentContext();

2. 拼接路径(下面代码是搞一条线段)
CGContextMoveToPoint(ctx, 10, 10);
CGContextAddLineToPoint(ctx, 100, 100);

3. 绘制路径
CGContextStrokePath(ctx); // CGContextFillPath(ctx);

(十)常用拼接路径的函数

新建一个起点
void CGContextMoveToPoint(CGContextRef c, CGFloat x, CGFloat y)

添加新的线段到某个点
void CGContextAddLineToPoint(CGContextRef c, CGFloat x, CGFloat y)

添加一个矩形
void CGContextAddRect(CGContextRef c, CGRect rect)

添加一个椭圆
void CGContextAddEllipseInRect(CGContextRef context, CGRect rect)

添加一个圆弧
void CGContextAddArc(CGContextRef c, CGFloat x, CGFloat y,
CGFloat radius, CGFloat startAngle, CGFloat endAngle, int clockwise)

(十一)常用绘制路径的函数

Mode参数决定绘制的模式
void CGContextDrawPath(CGContextRef c, CGPathDrawingMode mode)

绘制空心路径
void CGContextStrokePath(CGContextRef c)

绘制实心路径
void CGContextFillPath(CGContextRef c)

提示:一般以CGContextDraw、CGContextStroke、CGContextFill开头的函数,都是用来绘制路径的

(十二)上下文状态栈

上下文状态栈是上下文用来存储路径状态的,这里我们通过一个例子来说明上下文状态栈的原理

在view上画一个“十”字,一横为红色,宽度为10,一竖为绿色,宽度20 ???

错误示例:

let context = UIGraphicsGetCurrentContext()

let path = UIBezierPath()
path.move(to: CGPoint(x: 20, y: 100))
path.addLine(to: CGPoint(x: 180, y: 100))

context?.addPath(path.cgPath)

context?.setStrokeColor(UIColor.red.cgColor)
context?.setLineWidth(10)

let path1 = UIBezierPath()
path1.move(to: CGPoint(x: 100, y: 20))
path1.addLine(to: CGPoint(x: 100, y: 180))

context?.addPath(path1.cgPath)

context?.setStrokeColor(UIColor.green.cgColor)
context?.setLineWidth(20)

context?.strokePath()

首先分析一下为什么上面这段代码无法呈现出我们想要的效果

上面的代码首先获取和当前view相关联的上下文

let context = UIGraphicsGetCurrentContext()

然后,画两条需要的线,且分别添加到上下文上

let path = UIBezierPath()
path.move(to: CGPoint(x: 20, y: 100))
path.addLine(to: CGPoint(x: 180, y: 100))

context?.addPath(path.cgPath)

context?.setStrokeColor(UIColor.red.cgColor)
context?.setLineWidth(10)

let path1 = UIBezierPath()
path1.move(to: CGPoint(x: 100, y: 20))
path1.addLine(to: CGPoint(x: 100, y: 180))

context?.addPath(path1.cgPath)

context?.setStrokeColor(UIColor.green.cgColor)
context?.setLineWidth(20)

然后再把画好的两条线渲染到和当前上下文关联的view的layer上

context?.strokePath()

这里呈现的效果是两条颜色宽度都为绿色的线


image.png

为什么是两条绿色的线?是因为上下文的状态会被覆盖,第二次设置的状态会覆盖第一次设置的状态,导致上下文中所有的线都是最后一次设置的路径状态(宽度为20,颜色为绿色)

为了防止覆盖的问题,我们可以先画好一根线,再画第二根线

// 1. 获取当前view相关联的上下文
/*
 * 获取上下文相当于开辟了一块内存空间
 * 该内存空间分为两部分
 *  1. 存储路径
 *  2. 存储路径的状态(UIColor,Width)
 */
let context = UIGraphicsGetCurrentContext()

let path = UIBezierPath()
path.move(to: CGPoint(x: 100, y: 20))
path.addLine(to: CGPoint(x: 100, y: 180))

context?.addPath(path.cgPath)

context?.setStrokeColor(UIColor.red.cgColor)
context?.setLineWidth(10)

// 取出上下文中所有绘制的路径,并把上下文中存储的路径状态应用到所有的路径上,渲染到上下文关联的view的layer上
context?.strokePath()

/************************分割线************************/

let path1 = UIBezierPath()
path1.move(to: CGPoint(x: 20, y: 100))
path1.addLine(to: CGPoint(x: 180, y: 100))

context?.addPath(path1.cgPath)

context?.setStrokeColor(UIColor.green.cgColor)
context?.setLineWidth(20)

context?.strokePath()
image.png

通过上下文状态栈来实现类似效果
首先如果我们想要在一个view上通过Quartz 2D技术画路径(图),我们需要获取和当前view相关联的上下文(context)
获取上下文相当于在内存中开辟了一块存储空间。
该内存空间分为两部分,这里用A、B表示:
A部分. 存储绘制的路径
B部分. 存储路径的状态(路径的颜色、路径的宽度)

let context = UIGraphicsGetCurrentContext()

然后绘制路径,并添加到上下文中。这部分路径会存储在A部分

let path = UIBezierPath()
path.move(to: CGPoint(x: 20, y: 100))
path.addLine(to: CGPoint(x: 180, y: 100))

context?.addPath(path.cgPath)

再设置上下文中第二条路径(竖)的状态(颜色红,宽度为10)。这部分状态会存储在B部分

context?.setStrokeColor(UIColor.red.cgColor)
context?.setLineWidth(10)

我们先保存B部分的状态到上下文状态栈中

/*
 * 保存上下文中存储的状态到上下文状态栈中(栈:先进后出)
 * 保存了:color:red,width:10到上下文状态栈中
 */
context?.saveGState()

然后再设置第一条路径的状态(颜色绿,宽度20),并渲染到当前上下文关联的viewlayer

context?.setStrokeColor(UIColor.green.cgColor)
context?.setLineWidth(20)

context?.strokePath()

再绘制第二条路径,并取出上下文状态栈中保存的第一条路径的状态,并渲染到当前上下文关联的viewlayer

let path1 = UIBezierPath()
path1.move(to: CGPoint(x: 100, y: 20))
path1.addLine(to: CGPoint(x: 100, y: 180))

context?.addPath(path1.cgPath)

/*
 * 从当前上下文的状态栈中,取出栈顶的(最后存入的状态)状态,覆盖上下文中存储的状态
 */
context?.restoreGState()

context?.strokePath()

(十三)上下文的矩阵操作

let context = UIGraphicsGetCurrentContext()

let path = UIBezierPath(ovalIn: CGRect(x: 100, y: 100, width: 200, height: 130))

// 矩阵操作:平移
context?.translateBy(x: 0, y: 200)
// 矩阵操作:缩放
context?.scaleBy(x: 0.8, y: 1.3)
// 矩阵操作:旋转
context?.rotate(by: .pi/6)

// 注意:矩阵操作要在路径添加前进行,否则对所添加的路径无效
context?.addPath(path.cgPath)

context?.strokePath()
矩阵操作前

矩阵操作后

相关代码点我!!!

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

推荐阅读更多精彩内容