iOS UIBezierPath绘图相关问题

我们都知道可以通过UIBezierPath+CAShapeLayer来绘制我们想要的各种效果,本篇文章不再赘述这些问题,就说一下在使用这些类的时候,遇到的一些问题:

1. 在UIView或者其子类中,如果要使用CAShapeLayer,那么:不要使用懒加载!!不要使用懒加载!!不要使用懒加载

今天在项目中如此写的时候,懒加载了ShapeLayer,然后Layer就不能正常的绘制上去了

2. - (void)layoutSublayersOfLayer:(CALayer *)layer方法的使用

遇到在初始化的时候没有使用initWithFrame方法的情况,或者在外部使用Masonry设置约束的情况,

此时,我们可以在View的- (void)layoutSublayersOfLayer:(CALayer *)layer方法中设置frame相关参数

- (void)layoutSublayersOfLayer:(CALayer *)layer {
    _frameW = layer.frame.size.width;
    _frameH = layer.frame.size.height;
    _bigRadius = _frameH/2;
    _smallRadius = _bigRadius - kPVRadiusOffSet;
    
    [self setupLayers];
    self.progress = _progress;
}

- (void)setupLayers {
    self.backLayer = [CAShapeLayer layer];
    self.backLayer.fillColor = kPVBackgroundColor.CGColor;
    UIBezierPath *backPath = [UIBezierPath bezierPathWithRoundedRect: CGRectMake(0, kPVRadiusOffSet, _frameW, _smallRadius*2) cornerRadius: _smallRadius];
    self.backLayer.path = backPath.CGPath;
    [self.layer addSublayer:self.backLayer];
    
}

- (void)setProgress:(CGFloat)progress {
    _progress = progress;
    
    CGFloat currentX = _frameW * progress/100;
    
    self.gradientLayer = [CAShapeLayer layer];
    self.gradientLayer.fillColor = [UIColor yellowColor].CGColor;
    UIBezierPath *colorPath = [UIBezierPath bezierPath];
    [colorPath moveToPoint:CGPointMake(0, _bigRadius)];
    [colorPath addArcWithCenter:CGPointMake(_smallRadius, _bigRadius) radius:_smallRadius startAngle:M_PI endAngle:3*M_PI_2 clockwise:YES];
    [colorPath addLineToPoint:CGPointMake(currentX-20-2, kPVRadiusOffSet)];

3. UIBezierPath提供的绘制曲线的API

一共有两种:

  • addQuadCurveToPoint:controlPoint
    此方法绘制的是二次曲线,入参只需要一个controlPoint(控制点)

    二阶曲线的生成过程
  • addCurveToPoint:controlPoint1:controlPoint2:
    此方法绘制的是三次曲线,所以需要两个controlPoint

    三阶曲线的生成过程

控制点

贝塞尔曲线本质
本质上,贝塞尔曲线就是数学问题,我们可以使用公式来计算的

  1. 线性曲线


    线性曲线.png
  2. 二阶曲线


    二阶曲线.png
  3. 三阶曲线

摘自Wikipedia
Four points P0, P1, P2 and P3 in the plane or in higher-dimensional space define a cubic Bézier curve. The curve starts at P0 going toward P1 and arrives at P3 coming from the direction of P2. Usually, it will not pass through P1 or P2; these points are only there to provide directional information. The distance between P1 and P2 determines "how far" and "how fast" the curve moves towards P1 before turning towards P2.

Writing BPi,Pj,Pk(t) for the quadratic Bézier curve defined by points Pi, Pj, and Pk, the cubic Bézier curve can be defined as an affine combination of two quadratic Bézier curves:

大致意思如下:英语渣就不翻译了。。
四个点p0,p1,p2,p3能决定一个三阶曲线,
曲线从P0点开始,到P3结束,中间不会经过P1和P2点,P1,P2点只会提供一些方向性的信息,
P1P2之间的距离决定了曲线由向着P1转向P2的距离和速度


三阶曲线.png
  1. 高阶曲线。。。


    高阶曲线.gif

    大佬们自己去研究吧。。。

4. 提供一些网址,可以直观的展现贝塞尔曲线的效果

http://blogs.sitepointstatic.com/examples/tech/canvas-curves/bezier-curve.html

5. CAGradientLayer和CAShapeLayer的结合使用(做一个有渐变色底色不规则形状的进度条)

思路如下:
UIView和CALayer都有一个属性: mask
我们可以先使用CAShapeLayer绘制出个性形状的图像之后,
然后把这个图像当成view(或者Layer)的蒙版 --类似Sketch的遮罩

CALayer.h
/* A layer whose alpha channel is used as a mask to select between the
 * layer's background and the result of compositing the layer's
 * contents with its filtered background. Defaults to nil. When used as
 * a mask the layer's `compositingFilter' and `backgroundFilters'
 * properties are ignored. When setting the mask to a new layer, the
 * new layer must have a nil superlayer, otherwise the behavior is
 * undefined. Nested masks (mask layers with their own masks) are
 * unsupported. */

@property(nullable, strong) CALayer *mask;


UIView.h

@property(nullable, nonatomic,strong) UIView *maskView NS_AVAILABLE_IOS(8_0);

    CGFloat currentX = _frameW * progress/100;
    
    self.gradientLayer = [CAShapeLayer layer];
    self.gradientLayer.fillColor = [UIColor yellowColor].CGColor;
    UIBezierPath *colorPath = [UIBezierPath bezierPath];
    [colorPath moveToPoint:CGPointMake(0, _bigRadius)];
    [colorPath addArcWithCenter:CGPointMake(_smallRadius, _bigRadius) radius:_smallRadius startAngle:M_PI endAngle:3*M_PI_2 clockwise:YES];
    [colorPath addLineToPoint:CGPointMake(currentX-20-2, kPVRadiusOffSet)];
    //405 - 385 = 20px 的曲线
    //二阶曲线
//    [colorPath addQuadCurveToPoint:CGPointMake(currentX - 2, 0) controlPoint:CGPointMake(currentX - 10, kPVRadiusOffSet)];
//    [colorPath addLineToPoint:CGPointMake(currentX, 0)];
//    [colorPath addArcWithCenter:CGPointMake(currentX, bigRadius) radius:bigRadius startAngle:3*M_PI_2 endAngle:5*M_PI_2 clockwise:YES];
//    [colorPath addLineToPoint:CGPointMake(currentX - 2, frameH)];
//    [colorPath addQuadCurveToPoint:CGPointMake(currentX - 20 - 2, frameH - kPVRadiusOffSet) controlPoint:CGPointMake(currentX - 10, frameH - kPVRadiusOffSet)];
    //三阶曲线
    [colorPath addCurveToPoint:CGPointMake(currentX, 0) controlPoint1:CGPointMake(currentX - 15, kPVRadiusOffSet) controlPoint2:CGPointMake(currentX - 5, 0)];
    [colorPath addArcWithCenter:CGPointMake(currentX, _bigRadius) radius:_bigRadius startAngle:3*M_PI_2 endAngle:5*M_PI_2 clockwise:YES];
    [colorPath addCurveToPoint:CGPointMake(currentX - 20, _frameH - kPVRadiusOffSet) controlPoint1:CGPointMake(currentX - 5, _frameH) controlPoint2:CGPointMake(currentX - 15, _frameH - kPVRadiusOffSet)];
    
    [colorPath addLineToPoint:CGPointMake(_smallRadius, _frameH - kPVRadiusOffSet)];
    [colorPath addArcWithCenter:CGPointMake(_smallRadius, _bigRadius) radius:_smallRadius startAngle:M_PI_2 endAngle:M_PI clockwise:YES];
    
    [colorPath closePath];
    self.gradientLayer.path = colorPath.CGPath;
    
    //渐变色layer
    CAGradientLayer *gradientLayer = [CAGradientLayer layer];
    gradientLayer.colors = @[(__bridge id)[UIColor colorWithRed:76/255.0 green:194/255.0 blue:255/255.0 alpha:1].CGColor, (__bridge id)[UIColor colorWithRed:75/255.0 green:137/255.0 blue:230/255.0 alpha:1].CGColor];
    gradientLayer.locations = @[@0.3, @1.0];
    gradientLayer.startPoint = CGPointMake(0, 0);
    gradientLayer.endPoint = CGPointMake(1.0, 0);
    gradientLayer.frame = CGRectMake(0, 0, currentX+_bigRadius, _frameH);
    gradientLayer.mask = self.gradientLayer;
    [self.layer addSublayer:gradientLayer];

效果图如下:


我是效果图

6.两个UIBezierPath存在交叉的情况,或者要做一些“镂空”的效果,例如如下效果


此时,就会涉及到两个属性:

CAShapeLayer.h中

/* The fill rule used when filling the path. Options are `non-zero' and
 * `even-odd'. Defaults to `non-zero'. */

此处也有extern的const定义的名
/* `fillRule' values. */
CA_EXTERN NSString *const kCAFillRuleNonZero
    CA_AVAILABLE_STARTING (10.6, 3.0, 9.0, 2.0);
CA_EXTERN NSString *const kCAFillRuleEvenOdd
    CA_AVAILABLE_STARTING (10.6, 3.0, 9.0, 2.0);

@property(copy) NSString *fillRule;

和
UIBezierPath.h

@property(nonatomic) BOOL usesEvenOddFillRule; 
// Default is NO. When YES, the even-odd fill rule is used for drawing, clipping, and hit testing.

由此可以看到:
usesEvenOddFillRule属性默认为NO,如果为YES的时候,会用于绘制,剪切和响应链的判断

关于fillRule 摘自stackoverflow
The even-odd fill rule is one way to determine what regions of a path are "inside" the path vs "outside" the path, which is important to know when filling the interior of the path. They usually only differ when a path cuts holes in itself. The even-odd rule will usually not shade those regions, while the other option usually will.

The even-odd rule is simply this:

As you progress in a straight line across the canvas containing the path, 
count the number of times you cross the path. 
If you have crossed an odd number of times, 
you are "inside" the path. 
If you have crossed an even number of times, 
you are outside the path.

Another option is called the non-zero winding rule. (This is used if usesEvenOddFillRule is NO). The non-zero again considers a straight line across the path, but counts the intersections a bit differently. It takes into account the direction the path was drawn in. (i.e. a counter-clockwise circle is not the same as a clockwise circle.) It is thus:

As you progress in a straight line across the canvas containing the path, 
keep a counter, starting at 0. 
Every time you cross a portion of the path where the path progresses from your left to your right 
(as observed from the line crossing the path), 
add one to the counter. 
Every time you cross a line where the path progresses from your right to your left, 
subtract one from the counter. 
If the counter is non-zero, you are inside the path. Otherwise, you are outside.

简单来说:

fillRule属性,一共有两个值:

  • kCAFillRuleEvenOdd 和 even-odd相同
    我们把想要判断的点标注上去,然后从该点开始,做任意方向的射线,我们来查询射线与绘制的path的相交点,
    如果交点个数为奇数,则该点在path内部
    如果交点个数为偶数,则该点在path外部

  • kCAFillRuleNonZero 和 non-zero相同
    该模式,需要考虑绘图路径的方向
    我们还是从测试点出一个任意方向的射线,然后与path相交。计数从0开始
    我们查看绘制路径的方向,相对于射线的方向,
    如果绘制路径方向相对于射线是从左往右,则count加1
    如果绘制路径方向相对于射线是从右向左,则count减1
    计算最后Count的值,
    如果count为0,则该点在路径外边
    如果count不为0,则该点在路径里边

下面是一个五角星的实例,我们来看下两种规则的判断:


  • 先看nozero时,怎么判断点是否在内部区域。
    射线1和图形线段只有一个交点,为从左到右,所以结果是1,判定为内部区域;射线2有两个交点,都是从左到右,结果是2,内部区域;射线3有三个交点,两个从左到右,一个从右到左,结果是1,内部区域;射线4有四个交点,两个从左到右,两个从右到左,结果是0,所以是外部区域。

  • evenodd时,
    只判断交点个数,所以1,3都是内部区域,2,4都是外部区域。

一般来说,kCAFillRuleNonZerokCAFillRuleEvenOdd的结果是相反的(非常复杂的图形除外)

下面是上图所示的橘黄色圆环的实现:

    CAShapeLayer *shapelayer = [CAShapeLayer layer];

    UIBezierPath *bezierPath = [UIBezierPath bezierPath];
    UIBezierPath *outerCircle = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, 4, 4)];
    [outerCircle setLineWidth:2.0];
    [bezierPath appendPath:outerCircle];
    
    UIBezierPath *innerCircle = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, 30, 30)];
    [innerCircle setLineWidth:2.0];
    [bezierPath appendPath:innerCircle];
//    bezierPath.usesEvenOddFillRule = YES;
    
    shapelayer.path = bezierPath.CGPath;
    shapelayer.fillRule = @"even-odd";
//    shapelayer.fillRule = kCAFillRuleEvenOdd;
    shapelayer.fillColor = [UIColor orangeColor].CGColor;
    shapelayer.strokeColor = [UIColor yellowColor].CGColor;
    shapelayer.opacity = 0.5;
    [self.layer addSublayer:shapelayer];


如果有想要看源码的,请移步GitHub

如果您觉得此篇文章帮到了您,请给个Star,谢谢!

参考资料

https://stackoverflow.com/questions/6711707/draw-a-quadratic-b%C3%A9zier-curve-through-three-given-points

https://zhuanlan.zhihu.com/p/23381083

https://stackoverflow.com/questions/38723031/undestanding-uibezierpath-curving-mechanism-controlpoint-and-the-curve-point?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa

https://github.com/darkjoin/Learning/wiki/UIBezierPath%E7%BB%98%E5%9B%BE

https://stackoverflow.com/questions/14840563/how-does-usesevenoddfillrule-work

https://blog.csdn.net/mishiwjp/article/details/53484235

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,265评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,078评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,852评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,408评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,445评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,772评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,921评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,688评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,130评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,467评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,617评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,276评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,882评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,740评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,967评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,315评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,486评论 2 348

推荐阅读更多精彩内容