上一篇实战文章中了解了Quartz与UIKit API在绘图中的使用以及要注意的点.
在实际的开发中除非特殊情况,一般都会使用UIKit的API来进行绘图(UIKit 中将Quartz的很多复杂的C语言的API都进行了良好的封装), 但是如果有很复杂的绘图需求, UIKit就难以满足, 例如绘制渐变色图(使用CAGradientLayer也可以满足渐变背景, 但是灵活性不如直接绘图, 参考1).
先展示出本文要完成的结果:

1 原始Quartz 绘制线性渐变色的方法
#pragma mark 线性渐变
-(void)drawLinearGradient:(CGContextRef)context{
//使用rgb颜色空间
CGColorSpaceRef colorSpace=CGColorSpaceCreateDeviceRGB();
/*指定渐变色
space:颜色空间
components:颜色数组,注意由于指定了RGB颜色空间,那么四个数组元素表示一个颜色(red、green、blue、alpha),
如果有三个颜色则这个数组有4*3个元素
locations:颜色所在位置(范围0~1),这个数组的个数不小于components中存放颜色的个数
count:渐变个数,等于locations的个数
*/
CGFloat compoents[12]={
248.0/255.0,86.0/255.0,86.0/255.0,1,
249.0/255.0,127.0/255.0,127.0/255.0,1,
1.0,1.0,1.0,1.0
};
CGFloat locations[3]={0,0.3,1.0};
CGGradientRef gradient= CGGradientCreateWithColorComponents(colorSpace, compoents, locations, 3);
/*绘制线性渐变
context:图形上下文
gradient:渐变色
startPoint:起始位置
endPoint:终止位置
options:绘制方式,kCGGradientDrawsBeforeStartLocation 开始位置之前就进行绘制,到结束位置之后不再绘制,
kCGGradientDrawsAfterEndLocation开始位置之前不进行绘制,到结束点之后继续填充
*/
CGContextDrawLinearGradient(context, gradient, CGPointZero, CGPointMake(320, 300), kCGGradientDrawsAfterEndLocation);
//释放颜色空间
CGColorSpaceRelease(colorSpace);
}

2 使用Sketch和PaintCode快速得到path的绘制代码
参考 用Sketch和PaintCode快速得到绘制代码 获取飞燕的path路径UIBezierPath:
-(UIBezierPath *)bezierPath {
UIBezierPath* pathPath = [UIBezierPath bezierPath];
[pathPath moveToPoint: CGPointMake(123.87, 0.33)];
[pathPath addCurveToPoint: CGPointMake(176, 55.09) controlPoint1: CGPointMake(123.87, 0.33) controlPoint2: CGPointMake(157.86, 19.55)];
[pathPath addCurveToPoint: CGPointMake(184.49, 125.31) controlPoint1: CGPointMake(194.15, 90.63) controlPoint2: CGPointMake(184.49, 125.31)];
[pathPath addCurveToPoint: CGPointMake(198.82, 151.62) controlPoint1: CGPointMake(184.49, 125.31) controlPoint2: CGPointMake(194.74, 137.9)];
[pathPath addCurveToPoint: CGPointMake(198.82, 178.86) controlPoint1: CGPointMake(202.89, 165.35) controlPoint2: CGPointMake(198.82, 178.86)];
[pathPath addCurveToPoint: CGPointMake(187.11, 165.78) controlPoint1: CGPointMake(198.82, 178.86) controlPoint2: CGPointMake(194.39, 171.6)];
[pathPath addCurveToPoint: CGPointMake(171.75, 161.42) controlPoint1: CGPointMake(179.83, 159.95) controlPoint2: CGPointMake(171.75, 161.42)];
[pathPath addCurveToPoint: CGPointMake(154.86, 165.78) controlPoint1: CGPointMake(171.75, 161.42) controlPoint2: CGPointMake(166.11, 160.38)];
[pathPath addCurveToPoint: CGPointMake(133.83, 175.04) controlPoint1: CGPointMake(143.62, 171.18) controlPoint2: CGPointMake(133.83, 175.04)];
[pathPath addCurveToPoint: CGPointMake(88.54, 177.05) controlPoint1: CGPointMake(133.83, 175.04) controlPoint2: CGPointMake(114.72, 182.08)];
[pathPath addCurveToPoint: CGPointMake(41.29, 157.75) controlPoint1: CGPointMake(62.37, 172.02) controlPoint2: CGPointMake(41.29, 157.75)];
[pathPath addCurveToPoint: CGPointMake(22.36, 142.14) controlPoint1: CGPointMake(41.29, 157.75) controlPoint2: CGPointMake(34.53, 154.56)];
[pathPath addCurveToPoint: CGPointMake(0.04, 115.32) controlPoint1: CGPointMake(10.18, 129.71) controlPoint2: CGPointMake(0.04, 115.32)];
[pathPath addCurveToPoint: CGPointMake(44.26, 137.51) controlPoint1: CGPointMake(0.04, 115.32) controlPoint2: CGPointMake(27.4, 132.89)];
[pathPath addCurveToPoint: CGPointMake(80.01, 139.88) controlPoint1: CGPointMake(61.11, 142.14) controlPoint2: CGPointMake(80.01, 139.88)];
[pathPath addCurveToPoint: CGPointMake(98.37, 137.51) controlPoint1: CGPointMake(80.01, 139.88) controlPoint2: CGPointMake(92.48, 139.46)];
[pathPath addCurveToPoint: CGPointMake(112.82, 129) controlPoint1: CGPointMake(104.26, 135.56) controlPoint2: CGPointMake(112.82, 129)];
[pathPath addCurveToPoint: CGPointMake(67.8, 86.54) controlPoint1: CGPointMake(112.82, 129) controlPoint2: CGPointMake(94.31, 112.93)];
[pathPath addCurveToPoint: CGPointMake(18.99, 26.76) controlPoint1: CGPointMake(41.29, 60.14) controlPoint2: CGPointMake(18.99, 26.76)];
[pathPath addCurveToPoint: CGPointMake(64.71, 63.74) controlPoint1: CGPointMake(18.99, 26.76) controlPoint2: CGPointMake(49.4, 52.21)];
[pathPath addCurveToPoint: CGPointMake(98.37, 86.54) controlPoint1: CGPointMake(80.01, 75.26) controlPoint2: CGPointMake(98.37, 86.54)];
[pathPath addCurveToPoint: CGPointMake(70.49, 55.09) controlPoint1: CGPointMake(98.37, 86.54) controlPoint2: CGPointMake(85.81, 73.75)];
[pathPath addCurveToPoint: CGPointMake(44.26, 16.75) controlPoint1: CGPointMake(55.17, 36.43) controlPoint2: CGPointMake(44.26, 16.75)];
[pathPath addCurveToPoint: CGPointMake(94.1, 62.08) controlPoint1: CGPointMake(44.26, 16.75) controlPoint2: CGPointMake(77.08, 48.42)];
[pathPath addCurveToPoint: CGPointMake(141.42, 94.74) controlPoint1: CGPointMake(111.12, 75.74) controlPoint2: CGPointMake(141.42, 94.74)];
[pathPath addCurveToPoint: CGPointMake(145.4, 49.61) controlPoint1: CGPointMake(141.42, 94.74) controlPoint2: CGPointMake(148.4, 72.99)];
[pathPath addCurveToPoint: CGPointMake(123.87, 0.33) controlPoint1: CGPointMake(142.4, 26.22) controlPoint2: CGPointMake(123.87, 0.33)];
[pathPath closePath];
return pathPath;
}
3 CGGradientRef的Objective-C的封装
由于绘制渐变色需要使用CGGradientRef相关的API, 使用不方便.在<<iOS Drawing>>中提供了一个OC版本的封装:
#import <UIKit/UIKit.h>
#import "BezierFunctions.h"
#define COLOR_LEVEL(_selector_, _alpha_) [([UIColor _selector_])colorWithAlphaComponent:_alpha_]
#define WHITE_LEVEL(_amt_, _alpha_) [UIColor colorWithWhite:(_amt_) alpha:(_alpha_)]
// Gradient drawing styles
#define LIMIT_GRADIENT_EXTENT 0
#define BEFORE_START kCGGradientDrawsBeforeStartLocation
#define AFTER_END kCGGradientDrawsAfterEndLocation
#define KEEP_DRAWING kCGGradientDrawsAfterEndLocation | kCGGradientDrawsBeforeStartLocation
typedef __attribute__((NSObject)) CGGradientRef GradientObject;
@interface Gradient : NSObject
@property (nonatomic, readonly) CGGradientRef gradient;
+ (instancetype) gradientWithColors: (NSArray *) colors locations: (NSArray *) locations;
+ (instancetype) gradientFrom: (UIColor *) color1 to: (UIColor *) color2;
+ (instancetype) rainbow;
+ (instancetype) gradientUsingInterpolationBlock: (InterpolationBlock) block between: (UIColor *) c1 and: (UIColor *) c2;
+ (instancetype) easeInGradientBetween: (UIColor *) c1 and:(UIColor *) c2;
+ (instancetype) easeInOutGradientBetween: (UIColor *) c1 and:(UIColor *) c2;
+ (instancetype) easeOutGradientBetween: (UIColor *) c1 and:(UIColor *) c2;
- (void) drawFrom:(CGPoint) p1 toPoint: (CGPoint) p2 style: (int) mask;
- (void) drawRadialFrom:(CGPoint) p1 toPoint: (CGPoint) p2 radii: (CGPoint) radii style: (int) mask;
- (void) drawTopToBottom: (CGRect) rect;
- (void) drawBottomToTop: (CGRect) rect;
- (void) drawLeftToRight: (CGRect) rect;
- (void) drawFrom:(CGPoint) p1 toPoint: (CGPoint) p2;
- (void) drawAlongAngle: (CGFloat) angle in:(CGRect) rect;
- (void) drawBasicRadial: (CGRect) rect;
- (void) drawRadialFrom: (CGPoint) p1 toPoint: (CGPoint) p2;
@end;
#import "Drawing-Gradient.h"
#import "Utility.h"
#import <UIKit/UIKit.h>
@interface Gradient ()
@property (nonatomic, strong) GradientObject storedGradient;
@end
@implementation Gradient
#pragma mark - Internal
- (CGGradientRef) gradient
{
return _storedGradient;
}
#pragma mark - Convenience Creation
+ (instancetype) gradientWithColors: (NSArray *) colorsArray locations: (NSArray *) locationArray
{
if (!colorsArray) COMPLAIN_AND_BAIL_NIL(@"Missing colors array", nil);
if (!locationArray) COMPLAIN_AND_BAIL_NIL(@"Missing location array", nil);
CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
if (space == NULL)
{
NSLog(@"Error: Unable to create device RGB color space");
return nil;
}
// Convert locations to CGFloat *
CGFloat locations[locationArray.count];
for (int i = 0; i < locationArray.count; i++)
locations[i] = [locationArray[i] floatValue];
// Convert colors to (id) CGColorRef
NSMutableArray *colorRefArray = [NSMutableArray array];
for (UIColor *color in colorsArray)
[colorRefArray addObject:(id)color.CGColor];
CGGradientRef gradientRef = CGGradientCreateWithColors(space, (__bridge CFArrayRef) colorRefArray, locations);
CGColorSpaceRelease(space);
if (gradientRef == NULL)
{
NSLog(@"Error: Unable to construct CGGradientRef");
return nil;
}
Gradient *gradient = [[self alloc] init];
gradient.storedGradient = gradientRef;
CGGradientRelease(gradientRef);
return gradient;
}
+ (instancetype) gradientFrom: (UIColor *) color1 to: (UIColor *) color2
{
return [self gradientWithColors:@[color1, color2] locations:@[@(0.0f), @(1.0f)]];
}
#pragma mark - Linear
- (void) drawRadialFrom:(CGPoint) p1 toPoint: (CGPoint) p2 radii: (CGPoint) radii style: (int) mask
{
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL) COMPLAIN_AND_BAIL(@"No context to draw into", nil);
CGContextDrawRadialGradient(context, self.gradient, p1, radii.x, p2, radii.y, mask);
}
- (void) drawFrom:(CGPoint) p1 toPoint: (CGPoint) p2 style: (int) mask
{
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL) COMPLAIN_AND_BAIL(@"No context to draw into", nil);
CGContextDrawLinearGradient(context, self.gradient, p1, p2, mask);
}
- (void) drawLeftToRight: (CGRect) rect
{
CGPoint p1 = RectGetMidLeft(rect);
CGPoint p2 = RectGetMidRight(rect);
[self drawFrom:p1 toPoint:p2 style:KEEP_DRAWING];
}
- (void) drawTopToBottom: (CGRect) rect
{
CGPoint p1 = RectGetMidTop(rect);
CGPoint p2 = RectGetMidBottom(rect);
[self drawFrom:p1 toPoint:p2 style:KEEP_DRAWING];
}
- (void) drawBottomToTop:(CGRect)rect
{
CGPoint p1 = RectGetMidBottom(rect);
CGPoint p2 = RectGetMidTop(rect);
[self drawFrom:p1 toPoint:p2 style:KEEP_DRAWING];
}
- (void) drawFrom:(CGPoint) p1 toPoint: (CGPoint) p2
{
[self drawFrom:p1 toPoint:p2 style:KEEP_DRAWING];
}
- (void) drawAlongAngle: (CGFloat) theta in:(CGRect) rect
{
CGPoint center = RectGetCenter(rect);
CGFloat r = PointDistanceFromPoint(center, RectGetTopRight(rect));
CGFloat phi = theta + M_PI;
if (phi > TWO_PI)
phi -= TWO_PI;
CGFloat dx1 = r * sin(theta);
CGFloat dy1 = r * cos(theta);
CGFloat dx2 = r * sin(phi);
CGFloat dy2 = r * cos(phi);
CGPoint p1 = CGPointMake(center.x + dx1, center.y + dy1);
CGPoint p2 = CGPointMake(center.x + dx2, center.y + dy2);
[self drawFrom:p1 toPoint:p2];
}
#pragma mark - Radial
- (void) drawBasicRadial: (CGRect) rect
{
CGPoint p1 = RectGetCenter(rect);
CGFloat r = CGRectGetWidth(rect) / 2;
[self drawRadialFrom:p1 toPoint:p1 radii:CGPointMake(0, r) style:KEEP_DRAWING];
}
- (void) drawRadialFrom: (CGPoint) p1 toPoint: (CGPoint) p2;
{
[self drawRadialFrom:p1 toPoint:p1 radii:CGPointMake(0, PointDistanceFromPoint(p1, p2)) style:KEEP_DRAWING];
}
#pragma mark - Prebuilt
+ (instancetype) rainbow
{
NSMutableArray *colors = [NSMutableArray array];
NSMutableArray *locations = [NSMutableArray array];
int n = 24;
for (int i = 0; i <= n; i++)
{
CGFloat percent = (CGFloat) i / (CGFloat) n;
CGFloat colorDistance = percent * (CGFloat) (n - 1) / (CGFloat) n;
UIColor *color = [UIColor colorWithHue:colorDistance saturation:1 brightness:1 alpha:1];
[colors addObject:color];
[locations addObject:@(percent)];
}
return [Gradient gradientWithColors:colors locations:locations];
}
UIColor *InterpolateBetweenColors(UIColor *c1, UIColor *c2, CGFloat amt)
{
CGFloat r1, g1, b1, a1;
CGFloat r2, g2, b2, a2;
if (CGColorGetNumberOfComponents(c1.CGColor) == 4)
[c1 getRed:&r1 green:&g1 blue:&b1 alpha:&a1];
else
{
[c1 getWhite:&r1 alpha:&a1];
g1 = r1; b1 = r1;
}
if (CGColorGetNumberOfComponents(c2.CGColor) == 4)
[c2 getRed:&r2 green:&g2 blue:&b2 alpha:&a2];
else
{
[c2 getWhite:&r2 alpha:&a2];
g2 = r2; b2 = r2;
}
CGFloat r = (r2 * amt) + (r1 * (1.0 - amt));
CGFloat g = (g2 * amt) + (g1 * (1.0 - amt));
CGFloat b = (b2 * amt) + (b1 * (1.0 - amt));
CGFloat a = (a2 * amt) + (a1 * (1.0 - amt));
return [UIColor colorWithRed:r green:g blue:b alpha:a];
}
+ (instancetype) gradientUsingInterpolationBlock: (InterpolationBlock) block between: (UIColor *) c1 and: (UIColor *) c2;
{
if (!block)
COMPLAIN_AND_BAIL_NIL(@"Must pass interpolation block", nil);
NSMutableArray *colors = [NSMutableArray array];
NSMutableArray *locations = [NSMutableArray array];
int numberOfSamples = 24;
for (int i = 0; i <= numberOfSamples; i++)
{
CGFloat amt = (CGFloat) i / (CGFloat) numberOfSamples;
CGFloat percentage = Clamp(block(amt), 0.0, 1.0);
[colors addObject:InterpolateBetweenColors(c1, c2, percentage)];
[locations addObject:@(amt)];
}
return [Gradient gradientWithColors:colors locations:locations];
}
+ (instancetype) easeInGradientBetween: (UIColor *) c1 and:(UIColor *) c2
{
return [self gradientUsingInterpolationBlock:^CGFloat(CGFloat percent) {return EaseIn(percent, 3);} between:c1 and:c2];
}
+ (instancetype) easeInOutGradientBetween: (UIColor *) c1 and:(UIColor *) c2
{
return [self gradientUsingInterpolationBlock:^CGFloat(CGFloat percent) {return EaseInOut(percent, 3);} between:c1 and:c2];
}
+ (instancetype) easeOutGradientBetween: (UIColor *) c1 and:(UIColor *) c2
{
return [self gradientUsingInterpolationBlock:^CGFloat(CGFloat percent) {return EaseOut(percent, 3);} between:c1 and:c2];
}
@end
有了这个封装, 第一小节的绘制变成如下代码:
- (UIImage *)buildImage2:(CGSize)targetSize{
UIGraphicsBeginImageContextWithOptions(targetSize, YES, 0.0);
UIColor *fromColor = [UIColor blueColor];
UIColor *toColor = [UIColor greenColor];
Gradient *gradient = [Gradient gradientFrom:fromColor to:toColor];
[gradient drawTopToBottom:CGRectMake(0, 0, targetSize.width, targetSize.height)];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}

4. 结合前面的飞燕Path 绘制需求图片
- (UIImage *) buildImage3: (CGSize) targetSize {
// 1. 初始化Context
UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0.0);
CGRect targetRect = (CGRect){.size = targetSize};
[[UIColor clearColor] setFill];
UIRectFill(targetRect);
// 2. 获取飞燕Path
UIBezierPath *path = [self bezierPath];
// 3. 移动Path到整个targetSize的正中心, 并且修改Path的bounds
CGPoint position = RectGetCenter(targetRect);
FitPathToRect(path, self.maskFrameRect);
MovePathCenterToPoint(path, position);
// 4. 绘制飞燕Path外部渐变色从顶部到底部. 从红色到绿色
PushDraw(^{
// 4.1 先获取path的反向区域. 然后使用addClip, 后面操作的内容只会作用于 path.inverse 区域
[path.inverse addClip];
// 4.2 创建 Gradient
UIColor *fromColor = [UIColor redColor];
UIColor *toColor = [UIColor greenColor];
Gradient *gradient = [Gradient gradientFrom:fromColor to:toColor];
// 4.3 在 path.inverse 区域绘图
[gradient drawTopToBottom:CGRectMake(0, 0, targetSize.width,targetSize.height)];
});
// 5. 绘制飞燕Path内部 渐变色从顶部到底部. 从绿色到红色
PushDraw(^{
// 5.1 对path 使用addClip, 后面的操作只会作用于 path 包裹区域内
[path addClip];
// 5.2 创建Gradient
UIColor *fromColor = [UIColor greenColor];
UIColor *toColor = [UIColor redColor];
Gradient *gradient = [Gradient gradientFrom:fromColor to:toColor];
// 5.3 在 path 包裹的区域内绘图
[gradient drawTopToBottom:CGRectMake(0, 0, targetSize.width,targetSize.height)];
});
// 6 绘制飞燕的bounds
PushDraw(^{
[[UIColor blackColor] set];
UIBezierPath *roundPath = [UIBezierPath bezierPathWithRect:PathBoundingBox(path)];
[roundPath stroke];
});
// 7 绘制飞燕path的轮廓
PushDraw(^{
[[UIColor whiteColor] setStroke];
[path stroke];
});
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}

其中比较重要的概念是使用PushDraw
, 这个block能够让我们在每次修改context的属性时候能还原设置的状态.
void PushDraw(DrawingStateBlock block)
{
if (!block) return; // nothing to do
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL) COMPLAIN_AND_BAIL(@"No context to draw into", nil);
CGContextSaveGState(context);
block();
CGContextRestoreGState(context);
}
另外一个重点内容是 path.inverse
, 可以获取path路径外部空间:
#pragma mark - Inverses
- (UIBezierPath *) inverseInRect: (CGRect) rect
{
UIBezierPath *path = [UIBezierPath bezierPath];
CopyBezierState(self, path);
[path appendPath:self];
[path appendPath:[UIBezierPath bezierPathWithRect:rect]];
path.usesEvenOddFillRule = YES;
return path;
}
static UIBezierPath * extracted(UIBezierPath *object) {
return [object inverseInRect:CGRectInfinite];
}
- (UIBezierPath *) inverse
{
return extracted(self);
}
- (UIBezierPath *) boundedInverse
{
return [self inverseInRect:self.bounds];
}
全文Demo : https://github.com/brownfeng/QuartzWorkspace
参考
- ios实现颜色渐变的几种方法
- <<iOS Drawing>>

网友评论