美文网首页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