drawRect
前面文章介绍了寄宿图及相关的几个属性
除了通过 contents 设置 CGImage,还可以用 CoreGraphics 直接绘制图片。
CoreGraphics : 是一套基于C的框架,UIKit 库中所有 UI 组件都是由 CoreGraphics 绘制实现,因此它可以实现比UIKit更底层的功能。
要用 CoreGraphics 绘图,先自定义一个类, 继承 UIView,然后重写 drawRect 方法。
drawRect 方法文档中的一些重点如下:
UIView 默认实现为空,尽可能不要重写该方法,尽量用视图/图层。
为什么尽可能不要重写该方法呢?因为只要被调用了,就会给视图对应的 layer 分配一个相同大小的寄宿图!!!我们用 CoreGraphics 画完之后,是作为寄宿图呈现在图层上的,作为 layer.contents 保存起来,尽管保存的对象是我们不认识的 CABackingStore 类型;直到需要重新绘制或手动调用 setNeedsDisplay 重绘
当视图需要在屏幕上绘制,方法会被自动调用,不要自行调用该方法。该方法被调用时,UIKit 准备好了绘图上下文,代码里通过 UIGraphicsGetCurrentContext() 获得,不要想着持有这个 context 对象,因为其他视图 draw 方法被调用时,上下文变了。如果视图的 isOpaque 属性为 true,那自定义的 draw rect 最好也填充不透明的内容 (这个属于编码规范,不是硬性要求)。
如果的确需要重写draw rect,且继承自 UIView,可以不调用 super.draw(rect),但是如果继承自别的类,可能父类有一些自己的工作,还是要写上
1 画直线的例子
import UIKit
class CustomView: UIView {
override func draw(_ rect: CGRect) {
// 获取上下文,理解为 画笔
let cgContext = UIGraphicsGetCurrentContext()
// 一个矩形区域
let drawRect: CGRect = frame.insetBy(dx: 100, dy: 210)
let path = CGMutablePath()
// 下面想象着一条路径的移动:move 到一个点,加一条线到一个点·····
path.move(to: CGPoint(x: drawRect.minX, y: drawRect.minY))
path.addLine(to: CGPoint(x: drawRect.maxX, y: drawRect.minY))
path.addLine(to: CGPoint(x: drawRect.maxX, y: drawRect.maxY))
path.addLine(to: CGPoint(x: drawRect.minX, y: drawRect.maxY))
// 设置画笔参数,看函数名就知道意思
cgContext?.addPath(path)
cgContext?.setLineWidth(20)
cgContext?.setStrokeColor(UIColor.red.cgColor)
cgContext?.setShadow(offset: CGSize(width:3,height:3), blur: 15, color: UIColor.black.cgColor)
// 画 = stroke
cgContext?.strokePath()
// 再画一个
cgContext?.setStrokeColor(UIColor.green.cgColor)
cgContext?.stroke(drawRect.insetBy(dx: 40, dy: 50))
}
}
class ViewController: UIViewController {
var myView: CustomView!
override func viewDidLoad() {
super.viewDidLoad()
myView = CustomView(frame:view.bounds)
myView.backgroundColor = UIColor(white: 0.85, alpha: 1) // 默认黑
view.addSubview(myView)
}
}
效果:
还有一些函数,列几个:
cgContext?.setLineCap(.round) //顶点样式
cgContext?.setLineJoin(.round) //连接点
cgContext?.setShadow(offset: CGSize(width:3,height:3), blur: 10, color: UIColor.black.cgColor) // 阴影
cgContext?.setLineDash(phase: 0, lengths: [20,8]) // 画虚线,第一个参数写0,第二个数组是 实虚长度比例
2、画圆:
class CustomView: UIView {
override func draw(_ rect: CGRect) {
let cgContext = UIGraphicsGetCurrentContext()
let drawRect:CGRect = frame.insetBy(dx: 50, dy: 50)
let path = CGMutablePath()
//绘制圆弧必须确定:圆心的坐标、半径、圆弧的起点角度和终点角度(弧度表示),常量 CGFloat.pi 是 π,即180。
let radius = drawRect.width/2 //半径
let center = CGPoint(x: drawRect.midX, y: drawRect.midX)//圆心
//clockwise如果是true,画的是互补的那部分圆
path.addArc(center: center, radius: radius, startAngle: 0, endAngle: CGFloat.pi*1.5, clockwise: true)
cgContext?.setLineWidth(6)
cgContext?.setStrokeColor(UIColor.red.cgColor)
cgContext?.addPath(path)
cgContext?.strokePath()
}
}
绘制其他图形、其他用法,看函数名大致可以猜到,不详细介绍了。
cgContext?.setFillColor(UIColor.red.cgColor)
cgContext?.drawPath(using: .fillStroke)
path.addRoundedRect(in: CGRect, cornerWidth: CGFloat, cornerHeight: CGFloat)
path.addEllipse(in: CGRect)
path.addQuadCurve(to: CGPoint, control: CGPoint)
path.addCurve(to: CGPoint, control1: CGPoint, control2: CGPoint)
CALayerDelegate
前面提到,自己画的内容实际上是作为寄宿图展示的。
CALayer 有一个可选的 delegate 属性,UIView 自己的 layer 的 delegate 就是视图自己,最好不要修改,内部实现有一些额外操作;手动创建的 layer 可以指定其他对象作为 delegate;当 CALayer 对象被调用显示时,会请求它的代理(例如 UIView)给它一个寄宿图来显示。
如果实现了协议中这个方法(UIView 是实现了),执行后就完成了绘制,不再往下执行
func display(_ layer: CALayer) { }
自定义的代理可以在这个方法内直接设置 contents 属性等操作
如果没实现该方法,CALayer 会尝试调用下面这个协议方法:
func draw(_ layer: CALayer, in ctx: CGContext) {
//ctx.setLineWidth(10)
//ctx.setStrokeColor(UIColor.red.cgColor)
//ctx.strokeEllipse(in: layer.bounds)
}
如果 delegate 对这个协议方法 也没实现,图层就尝试调用自己的 draw(in:) 方法,绘制。
UIView 在 display(_ layer) 里做了所有工作,如 是否调用 drawRect、在合适的时机调用 layer.display() 等
demo:
class ViewController: UIViewController, CALayerDelegate {
var myView: UIView!
/// myView 的大小
let size: CGFloat = 200
/// 屏幕的宽度
let width = UIScreen.main.bounds.width
/// 屏幕的高度
let height = UIScreen.main.bounds.height
override func viewDidLoad() {
super.viewDidLoad()
myView = UIView(frame:view.bounds)
myView.backgroundColor = UIColor.lightGray
let subLayer = CALayer()
subLayer.frame = CGRect(x: size/4, y:size/4, width:size/2, height:size/2)
subLayer.backgroundColor = UIColor(red: 0.6, green:0.3, blue: 0.2, alpha:1).cgColor
subLayer.contentsScale = UIScreen.main.scale
subLayer.delegate = self
myView.layer.addSublayer(subLayer)
view.addSubview(myView)
// CALayer 的绘制需要手动调用 display(UIView 自己封装了自动绘制的逻辑)
subLayer.display()
}
// 先不实现这个方法
// func display(_ layer: CALayer) {}
// 实现了这个 CALayerDelegate 协议方法
func draw(_ layer: CALayer, in ctx: CGContext) {
ctx.setLineWidth(10)
ctx.setStrokeColor(UIColor.red.cgColor)
ctx.strokeEllipse(in: layer.bounds)
}
}
运行后发现有一处和预期不同:绘制的圆沿边界被裁剪了。但我们并没有 masksToBounds 啊?因为 CALayerDelegate 协议方法绘制时 不支持对超出边界外的内容绘制(CALayer 调用draw layer in ctx 时,ctx 只申请了 bounds 范围的位图)
总结
根本没必要用这个协议画图,也没有必要看这节。有 UIView::drawRect,CALayerDelegate 一般用不到...
况且实现 UIView::drawRect 后,UIView 会帮你做完剩下的工作,包括在需要重绘的时候调用 CALayer::display,减少编程失误。
那为什么有 CALayerDelegate 这个东西?部分原因是,macOS 上和 iOS 上的 CALayer 是通用的,但是二者视图层交互形式不一样,于是分别有 NSView、UIView 作为不同形式交互的实现。
下一篇:图层几何学 之 布局与锚点 和相关应用