iOS初级开发学习笔记:贝塞尔曲线的绘制学习

3,424 阅读5分钟

本文参考此文参考

我们在项目中会接触到一些曲线的绘制,最初接触这个概念是因为有一个在屏幕右边的按钮,因为贴着屏幕,按钮右边是没有圆角的,而左上和左下是圆角,类似:

还没知道贝塞尔曲线绘制之前,我是直接用 _laborExplainButton.layer.cornerRadius = 33/2; 绘制一个四个角都是圆角的按钮,然后给出文字靠左属性 _laborExplainButton.titleLabel.textAlignment = NSTextAlignmentLeft; 设定按钮宽度时,多给出一段宽度,在用Masonary布局时让右边超出父视图,再慢慢调数值,让文字接近居中。

方法很笨,但是还算是实现了图片的效果(笑)。

但是往后出现的一些类似于tableView的紧密相连的cell的第一个cell左上和右上为圆角的需求,总不能再这样“投机”了,就开始学习到了用贝塞尔曲线实现。


本笔记将从苹果官方API:UIBezierPath.h文件的各种方法解释、介绍出发,介绍常用的一些方法和流程,再对具体案例做实现介绍,做一个从学到用的总结,不能做到所有方法方面都顾及,更深的更复杂的绘制实现还待后续学习补充。


一、UIBezierPath的概念

UIBezierPath是在iOS开发中绘制矢量图或者路径的时候会经常使用的一个部分,在UIKit里面是CoreGraphics对path的封装,使用UIBezierPath可以绘制直线、矩形、椭圆、不规则图形、多边形和贝塞尔曲线等,只要是能想到的线条都能画出来。

二、UIBezierPath.h内方法介绍

// 用来对某(几)个角进行贝塞尔绘制
typedef NS_OPTIONS(NSUInteger, UIRectCorner) {
    UIRectCornerTopLeft     = 1 << 0,
    UIRectCornerTopRight    = 1 << 1,
    UIRectCornerBottomLeft  = 1 << 2,
    UIRectCornerBottomRight = 1 << 3,
    UIRectCornerAllCorners  = ~0UL
};


// 初始化无形装的贝塞尔曲线
+ (instancetype)bezierPath;
// 初始化矩形贝塞尔曲线
+ (instancetype)bezierPathWithRect:(CGRect)rect;
// 绘制椭圆(圆形)贝塞尔曲线
+ (instancetype)bezierPathWithOvalInRect:(CGRect)rect;
// 绘制含有圆角的贝塞尔曲线
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius; // rounds all corners with the same horizontal and vertical radius
// 绘制可选择圆角方位的贝塞尔曲线
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii;
// 绘制圆弧曲线
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
// 根据CGPathRef绘制贝塞尔曲线
+ (instancetype)bezierPathWithCGPath:(CGPathRef)CGPath;

- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;


// CGPath可以理解为图形的路径,拿到CGPath
- (CGPathRef)CGPath NS_RETURNS_INNER_POINTER CF_RETURNS_NOT_RETAINED;

// Path construction

// 贝塞尔曲线开始的点
- (void)moveToPoint:(CGPoint)point;
// 添加直线到该点
- (void)addLineToPoint:(CGPoint)point;
// 添加二次曲线到该点
- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2;
// 添加曲线到该点
- (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint;
// 添加圆弧
- (void)addArcWithCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise NS_AVAILABLE_IOS(4_0);
// 闭合曲线
- (void)closePath;

// 移除所有曲线的点
- (void)removeAllPoints;

// 路径拼接
- (void)appendPath:(UIBezierPath *)bezierPath;

// 返回一个与当前路径相反的新的贝塞尔路径对象
- (UIBezierPath *)bezierPathByReversingPath NS_AVAILABLE_IOS(6_0);

// 路径进行仿射变换
- (void)applyTransform:(CGAffineTransform)transform;

// Path info

// 只读类型,路径上是否有有效的元素
@property(readonly,getter=isEmpty) BOOL empty;
// 和view的bounds是不一样的,它获取path的X坐标、Y坐标、宽度,但是高度为0
@property(nonatomic,readonly) CGRect bounds;
// 当前path的位置,可以理解为path的终点
@property(nonatomic,readonly) CGPoint currentPoint;
// 路径是否包含点point
- (BOOL)containsPoint:(CGPoint)point;

// Drawing properties

// 边框高度
@property(nonatomic) CGFloat lineWidth;
// 端点类型
@property(nonatomic) CGLineCap lineCapStyle;
// 线条连接类型
@property(nonatomic) CGLineJoin lineJoinStyle;
// 线条最大宽度最大限制
@property(nonatomic) CGFloat miterLimit; // Used when lineJoinStyle is kCGLineJoinMiter
// 绘制的精度,默认为0.6,精度越大需要处理的时间越长
@property(nonatomic) CGFloat flatness;
// 单双数圈规则是否用于绘制路径,默认是NO
@property(nonatomic) BOOL usesEvenOddFillRule; // Default is NO. When YES, the even-odd fill rule is used for drawing, clipping, and hit testing.

// 设置线型
- (void)setLineDash:(nullable const CGFloat *)pattern count:(NSInteger)count phase:(CGFloat)phase;
// 检索线型
- (void)getLineDash:(nullable CGFloat *)pattern count:(nullable NSInteger *)count phase:(nullable CGFloat *)phase;

// Path operations on the current graphics context

// 填充贝塞尔曲线内部
- (void)fill;
// 绘制贝塞尔曲线边框
- (void)stroke;

// These methods do not affect the blend mode or alpha of the current graphics context
// 过于复杂
- (void)fillWithBlendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;
- (void)strokeWithBlendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;

// 修改当前图形上下文的绘图区域可见,随后的绘图操作导致呈现内容只有发生在指定路径的填充区域
- (void)addClip;

三、UIBezierPath的使用

UIBezierPath是对CGPathRef的封装,它提供了CGPath属性使我们在开发过程中获取底层的path,在创建矢量图形的时候,把图形拆解成一条或者多条线段,然后拼接在一起,每条线段的终点都是下一条线段的起点,这就是大概的实现思路。具体步骤如下:

1、创建一个UIBezierPath对象;

2、用moveToPoint:方法设置初始线段的起点;

3、添加线段,定义一个或者多个子路径;

4、修改UIBezierPathUIBezierPath的绘图部分的相关属性;

一个cell顶部两角切圆角的简单实现:

效果图:

图片是个UIImageView,起初对背景的白色view切了四个角的圆角,发现这个UIImageView还是四角尖尖,再对UIImageView切圆角,变成了四角圆圆....所以,在对背景view切好四个角圆角后,我们还要用贝塞尔,切UIImageView的上两个角圆角:

但是,我用的是Masonary布局,在懒加载中写贝塞尔相关属性,并不能实现,因为一开始不能得到UIImageView的布局,所以研究了一番,我们在- (void)layoutSubviews;方法中才去写切圆角操作,这个方法是在布局完之后会走一次的,很好的解决了取不到范围的问题。

具体:

- (void)layoutSubviews{
    [super layoutSubviews];
    //  切上上两角圆角
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.coverImageView.bounds byRoundingCorners: UIRectCornerTopLeft|UIRectCornerTopRight cornerRadii:CGSizeMake(5, 5)];
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
    // frame为UIImageView的bounds
    maskLayer.frame = self.coverImageView.bounds;
    maskLayer.path = maskPath.CGPath;
    self.coverImageView.layer.mask = maskLayer;
}

项目中其他类似的情况都触类旁通,使用此方法解决。

这里其实是UIBezierPath结合其他layer使用。 原则上使用UIBezierPath主要只是画出形状或画出一个图形的路径path,但是它也可以配合其他的layer使用(CAShapeLayer,CAGradientLayer等),layer可以添加动画,所以UIBezierPath结合layer使用效果会更棒。