美文网首页ios进阶iOS进阶高级
iOS 绘图学习实战(二) -- 绘制渐变图

iOS 绘图学习实战(二) -- 绘制渐变图

作者: brownfeng | 来源:发表于2018-01-26 11:06 被阅读25次

上一篇实战文章中了解了Quartz与UIKit API在绘图中的使用以及要注意的点.

在实际的开发中除非特殊情况,一般都会使用UIKit的API来进行绘图(UIKit 中将Quartz的很多复杂的C语言的API都进行了良好的封装), 但是如果有很复杂的绘图需求, UIKit就难以满足, 例如绘制渐变色图(使用CAGradientLayer也可以满足渐变背景, 但是灵活性不如直接绘图, 参考1).

先展示出本文要完成的结果:

image.png

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);
}
image.png

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;
}
image.png

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;
}
image.png

其中比较重要的概念是使用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

参考

image.png

相关文章

网友评论

    本文标题:iOS 绘图学习实战(二) -- 绘制渐变图

    本文链接:https://www.haomeiwen.com/subject/oabmaxtx.html