iOS开发基础32-Quartz2D(二)绘图实战:画线、饼图、雪花动画、水印、裁剪与截图
Quartz 2D 绘图实战:画线、饼图、雪花动画、水印、裁剪与截图
本文通过 12 个实战案例,系统掌握 Quartz 2D 与 UIKit 绘图:直线/曲线/扇形绘制、setNeedsDisplay 重绘机制、下载进度圆、饼图、文字与图片绘制、CADisplayLink 雪花动画、上下文状态栈、矩阵变换、图片水印、圆形裁剪带圆环、屏幕截图、区域截图、图片擦除。
一、画线
1. 一条直线
绘图必须在 drawRect: 中进行,只有在此方法中才能获取与 View 关联的图形上下文。
- (void)drawRect:(CGRect)rect {
// 1. 获取上下文
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 2. 拼接路径(UIBezierPath 封装了常用路径)
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(50, 50)]; // 起点
[path addLineToPoint:CGPointMake(200, 200)]; // 终点
// 3. 将路径添加到上下文
CGContextAddPath(ctx, path.CGPath);
// 4. 渲染(描边)
CGContextStrokePath(ctx);
}
UIBezierPath可直接调用stroke/fill,无需手动CGContextAddPath+CGContextStrokePath:UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointMake(50, 50)]; [path addLineToPoint:CGPointMake(200, 200)]; [path stroke]; // 直接描边
2. 两条不相连的线
一个 UIBezierPath 可保存多条线段。若线段不相连,重新调用 moveToPoint: 设置新起点:
- (void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 第一条线
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(10, 10)];
[path addLineToPoint:CGPointMake(20, 20)];
CGContextAddPath(ctx, path.CGPath);
// 第二条线(不相连,重新设置起点)
path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(50, 50)];
[path addLineToPoint:CGPointMake(20, 200)];
CGContextAddPath(ctx, path.CGPath);
CGContextStrokePath(ctx);
}
不相连的线段建议各自使用一个路径对象,逻辑更清晰。
3. 绘制曲线(二次贝塞尔)
- (void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(10, 125)];
// 二次贝塞尔曲线:终点 + 控制点
[path addQuadCurveToPoint:CGPointMake(240, 125) controlPoint:CGPointMake(125, 240)];
[path addLineToPoint:CGPointMake(10, 125)];
CGContextAddPath(ctx, path.CGPath);
// 设置绘图状态(必须在渲染之前)
[[UIColor redColor] setStroke];
CGContextSetLineWidth(ctx, 15);
CGContextSetLineCap(ctx, kCGLineCapRound); // 圆角端点
CGContextSetLineJoin(ctx, kCGLineJoinRound); // 圆角连接
CGContextStrokePath(ctx);
}
4. 画扇形
- (void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGPoint center = CGPointMake(self.bounds.size.width * 0.5, self.bounds.size.height * 0.5);
// 圆弧:圆心、半径、起始角、结束角、是否顺时针
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center
radius:100
startAngle:0
endAngle:M_PI_2
clockwise:YES];
[path addLineToPoint:center]; // 连接到圆心
[path closePath]; // 关闭路径(从终点连回起点)
CGContextAddPath(ctx, path.CGPath);
[[UIColor redColor] setFill];
[[UIColor greenColor] setStroke];
CGContextSetLineWidth(ctx, 5);
// 同时填充和描边
CGContextDrawPath(ctx, kCGPathFillStroke);
}
fill会自动关闭路径(即使未调用closePath);stroke不会自动关闭,需手动closePath才会绘制最后一条边。

二、重绘机制与下载进度

drawRect: 只在 View 首次显示时调用一次。如需更新绘制,必须调用 setNeedsDisplay 标记重绘,系统在下一次屏幕刷新时自动调用 drawRect:。
自定义进度 View
@interface ProgressView : UIView
@property (nonatomic, assign) CGFloat progress; // 0.0 ~ 1.0
@end
@implementation ProgressView
- (void)setProgress:(CGFloat)progress {
_progress = progress;
// 不能手动调用 drawRect:,必须通过 setNeedsDisplay 触发
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
CGPoint center = CGPointMake(self.bounds.size.width * 0.5, self.bounds.size.width * 0.5);
CGFloat radius = self.bounds.size.width * 0.5 - 2;
CGFloat startA = -M_PI_2; // 从 12 点钟方向开始
CGFloat endA = -M_PI_2 + _progress * M_PI * 2; // 根据进度计算结束角
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:startA
endAngle:endA
clockwise:YES];
path.lineWidth = 5;
[[UIColor blueColor] setStroke];
[path stroke];
}
@end
控制器中使用
- (IBAction)sliderValueChanged:(UISlider *)sender {
self.label.text = [NSString stringWithFormat:@"%.2f%%", sender.value * 100];
self.progressView.progress = sender.value; // 内部触发 setNeedsDisplay
}
setNeedsDisplay底层不会立即调用drawRect:,而是给控件绑定一个"需要重绘"的标识,每次屏幕刷新(约 60 次/秒)时统一重绘所有绑定了标识的控件。
三、画饼图

@implementation PieView
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self setNeedsDisplay]; // 点击重新绘制(随机颜色)
}
- (void)drawRect:(CGRect)rect {
NSArray *data = @[@25, @25, @20, @30]; // 各部分占比(总和 100)
CGPoint center = CGPointMake(self.bounds.size.width * 0.5, self.bounds.size.height * 0.5);
CGFloat radius = self.bounds.size.width * 0.5;
CGFloat startA = 0;
CGFloat endA = 0;
CGFloat angle = 0;
for (NSNumber *num in data) {
startA = endA;
angle = [num intValue] / 100.0 * M_PI * 2; // 占比对应的弧度
endA = startA + angle;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:startA
endAngle:endA
clockwise:YES];
[path addLineToPoint:center];
[[self randomColor] set]; // set 同时设置描边和填充颜色
[path fill];
}
}
- (UIColor *)randomColor {
CGFloat r = arc4random_uniform(256) / 255.0;
CGFloat g = arc4random_uniform(256) / 255.0;
CGFloat b = arc4random_uniform(256) / 255.0;
return [UIColor colorWithRed:r green:g blue:b alpha:1];
}
@end
四、UIKit 绘图(文字与图片)
1. 绘制文字(富文本属性)
- (void)drawRect:(CGRect)rect {
NSString *str = @"hello!";
NSMutableDictionary *attrs = [NSMutableDictionary dictionary];
attrs[NSFontAttributeName] = [UIFont boldSystemFontOfSize:50];
attrs[NSStrokeWidthAttributeName] = @1; // 描边宽度
attrs[NSStrokeColorAttributeName] = [UIColor redColor]; // 描边颜色
attrs[NSForegroundColorAttributeName] = [UIColor redColor]; // 文字颜色
NSShadow *shadow = [[NSShadow alloc] init];
shadow.shadowColor = [UIColor yellowColor];
shadow.shadowOffset = CGSizeMake(10, 10);
shadow.shadowBlurRadius = 5;
attrs[NSShadowAttributeName] = shadow;
[str drawAtPoint:CGPointZero withAttributes:attrs];
}
2. 绘制图片与裁剪
- (void)drawRect:(CGRect)rect {
UIImage *image = [UIImage imageNamed:@"001"];
// 裁剪:必须在绘制之前设置,超出裁剪区域的内容被裁掉
UIRectClip(CGRectMake(0, 0, 50, 50));
// 按原图尺寸绘制
[image drawAtPoint:CGPointZero];
// 拉伸到指定区域绘制
// [image drawInRect:CGRectMake(0, 0, 100, 100)];
// 平铺绘制
// [image drawAsPatternInRect:rect];
}
五、雪花动画与 CADisplayLink

需要频繁重绘的动画,推荐使用 CADisplayLink 而非 NSTimer:CADisplayLink 与屏幕刷新同步(60 次/秒),不会出现延迟或掉帧。
@implementation SnowView
- (void)awakeFromNib {
// CADisplayLink:屏幕每次刷新时调用(约 60 次/秒)
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(setNeedsDisplay)];
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
// 停止时调用 [link invalidate];
}
- (void)drawRect:(CGRect)rect {
static CGFloat snowY = 0;
UIImage *image = [UIImage imageNamed:@"雪花"];
[image drawAtPoint:CGPointMake(0, snowY)];
snowY += 10;
if (snowY > rect.size.height) {
snowY = 0;
}
}
@end
NSTimer受 RunLoop 模式影响,在 UIScrollView 滚动时会暂停;CADisplayLink同样受 RunLoop 模式影响,需添加到NSRunLoopCommonModes才能在滚动时继续执行。
六、图形上下文状态栈
CGContextSaveGState 保存当前上下文状态(颜色、线宽、变换矩阵等),CGContextRestoreGState 恢复,用于临时修改状态而不影响后续绘制:
- (void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 第一条线
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(10, 125)];
[path addLineToPoint:CGPointMake(240, 125)];
CGContextAddPath(ctx, path.CGPath);
CGContextSaveGState(ctx); // 保存默认状态
[[UIColor redColor] set];
CGContextSetLineWidth(ctx, 20);
CGContextStrokePath(ctx);
// 第二条线
path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(125, 10)];
[path addLineToPoint:CGPointMake(125, 240)];
CGContextAddPath(ctx, path.CGPath);
CGContextRestoreGState(ctx); // 恢复到保存时的状态(红色和20线宽被撤销)
CGContextStrokePath(ctx); // 使用默认状态(黑色、1pt 线宽)绘制
}
Save 与 Restore 必须成对出现,保存几次就要恢复几次。

七、矩阵操作(CTM)
CTM(当前变换矩阵)作用于上下文所有后续绘制,支持平移、旋转、缩放:
- (void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 矩阵操作顺序很重要:先平移再旋转 ≠ 先旋转再平移
CGContextTranslateCTM(ctx, 150, 200); // 平移
CGContextRotateCTM(ctx, M_PI_4); // 旋转 45°
CGContextScaleCTM(ctx, 0.5, 0.5); // 缩放 0.5 倍
// 绘制一个椭圆(以原点为中心,配合平移使用)
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(-50, -100, 100, 200)];
[[UIColor redColor] set];
[path fill];
}
矩阵变换是累积的,且作用于后续所有绘制。如需临时变换,配合
CGContextSaveGState/RestoreGState使用。
八、图片水印
利用位图图形上下文,将文字或 Logo 绘制到图片上,生成新图片:
- (void)viewDidLoad {
[super viewDidLoad];
UIImage *image = [UIImage imageNamed:@"小黄人"];
// 1. 开启位图上下文
// size: 上下文尺寸;opaque: 是否不透明;scale: 0 自动取屏幕 scale
UIGraphicsBeginImageContextWithOptions(image.size, NO, 0);
// 2. 绘制原图
[image drawAtPoint:CGPointZero];
// 3. 绘制水印文字
NSString *str = @"超神五杀怪我咯";
[str drawAtPoint:CGPointZero withAttributes:@{NSForegroundColorAttributeName: [UIColor redColor]}];
// 4. 从上下文生成新图片
UIImage *watermarkedImage = UIGraphicsGetImageFromCurrentImageContext();
// 5. 结束上下文
UIGraphicsEndImageContext();
// 6. 保存到沙盒(PNG 无损,JPG 有损)
NSData *data = UIImageJPEGRepresentation(watermarkedImage, 0.8);
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"watermark.jpg"];
[data writeToFile:path atomically:YES];
}
UIGraphicsBeginImageContext(无 Options)默认 scale=1,在 Retina 屏上模糊;推荐始终使用UIGraphicsBeginImageContextWithOptions并设 scale=0。

九、圆形裁剪(带圆环)

通过分类方法,生成带圆环的圆形头像图片:
@interface UIImage (Circle)
+ (instancetype)circleImageWithName:(NSString *)name
borderColor:(UIColor *)color
borderWidth:(CGFloat)border;
@end
@implementation UIImage (Circle)
+ (instancetype)circleImageWithName:(NSString *)name
borderColor:(UIColor *)color
borderWidth:(CGFloat)border {
UIImage *image = [UIImage imageNamed:name];
// 上下文尺寸 = 图片尺寸 + 2 倍边框宽度
CGFloat contextW = image.size.width + 2 * border;
CGFloat contextH = image.size.height + 2 * border;
CGSize contextSize = CGSizeMake(contextW, contextH);
UIGraphicsBeginImageContextWithOptions(contextSize, NO, 0);
// 1. 画大圆(作为圆环)
UIBezierPath *bigCircle = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, contextW, contextH)];
[color set];
[bigCircle fill];
// 2. 设置裁剪区域(小圆)
UIBezierPath *clipPath = [UIBezierPath bezierPathWithOvalInRect:
CGRectMake(border, border, image.size.width, image.size.height)];
[clipPath addClip]; // 后续绘制被限制在小圆内
// 3. 绘制原图(被裁剪为圆形)
[image drawAtPoint:CGPointMake(border, border)];
// 4. 生成新图片
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}
@end
十、屏幕截图
方式一:renderInContext:(兼容旧系统)
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 将 view 的 layer 渲染到上下文
[self.view.layer renderInContext:ctx];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
NSData *data = UIImagePNGRepresentation(image);
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"screenshot.png"];
[data writeToFile:path atomically:YES];
}
方式二:drawViewHierarchyInRect:afterScreenUpdates:(iOS 7+,推荐)
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);
[self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:YES];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
drawViewHierarchyInRect:比renderInContext:性能更好,且能正确捕获 UIVisualEffectView 模糊等特效。iOS 7+ 推荐使用。
十一、区域截图(QQ 截图效果)


拖拽选择区域,裁剪出对应部分:
@interface ScreenshotViewController ()
@property (nonatomic, weak) UIView *cover; // 半透明蒙板
@property (nonatomic, assign) CGPoint startPoint; // 起始点
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation ScreenshotViewController
- (UIView *)cover {
if (!_cover) {
UIView *view = [[UIView alloc] init];
view.backgroundColor = [UIColor blackColor];
view.alpha = 0.5;
[self.view addSubview:view];
_cover = view;
}
return _cover;
}
- (IBAction)pan:(UIPanGestureRecognizer *)sender {
CGPoint curP = [sender locationInView:self.imageView];
if (sender.state == UIGestureRecognizerStateBegan) {
self.startPoint = curP;
}
// 更新蒙板 frame
CGFloat w = curP.x - self.startPoint.x;
CGFloat h = curP.y - self.startPoint.y;
self.cover.frame = CGRectMake(self.startPoint.x, self.startPoint.y, w, h);
if (sender.state == UIGestureRecognizerStateEnded) {
// 裁剪选中区域
UIGraphicsBeginImageContextWithOptions(self.imageView.bounds.size, NO, 0);
UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.cover.frame];
[path addClip]; // 裁剪区域
[self.imageView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self.imageView.image = image;
[self.cover removeFromSuperview];
}
}
@end
十二、图片擦除
手指拖动时,擦除上层图片的对应区域,露出下层图片:


- (IBAction)pan:(UIPanGestureRecognizer *)sender {
CGPoint curP = [sender locationInView:sender.view];
// 擦除区域(以触摸点为中心的 30x30 正方形)
CGFloat wh = 30;
CGFloat x = curP.x - wh * 0.5;
CGFloat y = curP.y - wh * 0.5;
CGRect eraseRect = CGRectMake(x, y, wh, wh);
UIGraphicsBeginImageContextWithOptions(sender.view.bounds.size, NO, 0);
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 将当前图片渲染到上下文
[sender.view.layer renderInContext:ctx];
// 清除指定区域(透明)
CGContextClearRect(ctx, eraseRect);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 更新显示
UIImageView *imageV = (UIImageView *)sender.view;
imageV.image = image;
}
CGContextClearRect会将指定区域清除为透明,需确保上下文opaque=NO(不透明为 NO),否则清除区域为黑色。
十三、Swift 版本对照
画线
class DrawView: UIView {
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
path.move(to: CGPoint(x: 50, y: 50))
path.addLine(to: CGPoint(x: 200, y: 200))
path.lineWidth = 2
UIColor.red.setStroke()
path.stroke()
}
}
下载进度
class ProgressView: UIView {
var progress: CGFloat = 0 {
didSet { setNeedsDisplay() }
}
override func draw(_ rect: CGRect) {
let center = CGPoint(x: bounds.width / 2, y: bounds.width / 2)
let radius = bounds.width / 2 - 2
let startA = -CGFloat.pi / 2
let endA = startA + progress * 2 * CGFloat.pi
let path = UIBezierPath(arcCenter: center, radius: radius,
startAngle: startA, endAngle: endA, clockwise: true)
path.lineWidth = 5
UIColor.blue.setStroke()
path.stroke()
}
}
雪花动画(CADisplayLink)
class SnowView: UIView {
private var snowY: CGFloat = 0
override func awakeFromNib() {
super.awakeFromNib()
let link = CADisplayLink(target: self, selector: #selector(setNeedsDisplay))
link.add(to: .main, forMode: .common)
}
override func draw(_ rect: CGRect) {
if let image = UIImage(named: "雪花") {
image.draw(at: CGPoint(x: 0, y: snowY))
}
snowY += 10
if snowY > rect.height { snowY = 0 }
}
}
图片水印(UIGraphicsImageRenderer,iOS 10+)
func watermark(image: UIImage, text: String) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: image.size)
return renderer.image { _ in
image.draw(at: .zero)
let attrs: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.red]
text.draw(at: .zero, withAttributes: attrs)
}
}
圆形裁剪带圆环
extension UIImage {
static func circle(name: String, borderColor: UIColor, borderWidth: CGFloat) -> UIImage? {
guard let image = UIImage(named: name) else { return nil }
let contextW = image.size.width + 2 * borderWidth
let contextH = image.size.height + 2 * borderWidth
let size = CGSize(width: contextW, height: contextH)
let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { _ in
// 大圆(圆环)
let bigCircle = UIBezierPath(ovalIn: CGRect(origin: .zero, size: size))
borderColor.setFill()
bigCircle.fill()
// 裁剪区域
let clipRect = CGRect(x: borderWidth, y: borderWidth,
width: image.size.width, height: image.size.height)
UIBezierPath(ovalIn: clipRect).addClip()
// 绘制原图
image.draw(at: CGPoint(x: borderWidth, y: borderWidth))
}
}
}
屏幕截图
func screenshot(view: UIView) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: view.bounds.size)
return renderer.image { _ in
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
}
}
图片擦除
@IBAction func pan(_ sender: UIPanGestureRecognizer) {
guard let view = sender.view else { return }
let curP = sender.location(in: view)
let wh: CGFloat = 30
let eraseRect = CGRect(x: curP.x - wh/2, y: curP.y - wh/2, width: wh, height: wh)
let renderer = UIGraphicsImageRenderer(size: view.bounds.size)
let image = renderer.image { ctx in
view.layer.render(in: ctx.cgContext)
ctx.cgContext.clear(eraseRect)
}
if let imageView = view as? UIImageView {
imageView.image = image
}
}
十四、总结
- 画线:
UIBezierPath拼接路径(moveToPoint/addLineToPoint/addQuadCurveToPoint/bezierPathWithArcCenter),通过stroke/fill渲染;不相连的线段需重新moveToPoint。 - 重绘机制:
drawRect:只调用一次,更新需通过setNeedsDisplay触发;不可手动调用drawRect:。 - 下载进度圆:起始角
-M_PI_2(12 点方向),结束角随进度变化,setter 中调用setNeedsDisplay。 - 饼图:遍历数据,每段扇形从
startA到endA,addLineToPoint:center连接圆心,fill自动关闭路径。 - UIKit 绘图:
NSString drawAtPoint:withAttributes:绘制富文本;UIImage drawAtPoint:/drawInRect:绘制图片;UIRectClip设置裁剪区域。 - CADisplayLink:与屏幕刷新同步(60fps),适合频繁重绘的动画;需添加到
NSRunLoopCommonModes才能在 UIScrollView 滚动时继续执行。 - 上下文栈:
CGContextSaveGState/CGContextRestoreGState成对使用,临时修改绘图状态。 - 矩阵变换:CTM 支持 Translate/Rotate/Scale,累积生效,顺序敏感,配合上下文栈临时使用。
- 图片水印:
UIGraphicsBeginImageContextWithOptions开启位图上下文,绘制原图+水印,UIGraphicsGetImageFromCurrentImageContext生成新图。 - 圆形裁剪:先画大圆(圆环),再设置小圆裁剪区域,最后绘制原图;
addClip限制后续绘制区域。 - 屏幕截图:
renderInContext:(旧)或drawViewHierarchyInRect:afterScreenUpdates:(iOS 7+ 推荐,性能好、支持模糊特效)。 - 区域截图:半透明蒙板指示选区,
addClip裁剪后渲染 layer。 - 图片擦除:
CGContextClearRect清除指定区域为透明,上下文需opaque=NO。 - 现代 API:iOS 10+ 推荐
UIGraphicsImageRenderer,自动管理上下文生命周期,无需手动 Begin/End。

浙公网安备 33010602011771号