Objective-C的UIBezierPath与CAShapeLayer学习笔记

UIBezierPath

UIBezierPath是UIKit 中的一个类,继承自NSObject,可以创建基于矢量的路径,此类是Core Graphics(Core Graphics是Quartz 2D的一个高级绘图引擎)框架关于path的一个OC封装,中文叫贝塞尔曲线。此类仅指定路径的几何图形,路径可以定义简单的形状,如矩形、椭圆和圆弧,也可以定义包含直线段和曲线段的复杂多边形。每一个直线段或者曲线段的结束的地方是下一个的开始的地方,每一个连接的直线或者曲线段的集合成为subpath,一个UIBezierPath对象定义一个完整的路径包括一个或者多个subpaths。

CAShapeLayer

CAShapeLayer继承自CALayer。CAShapeLayer属于CoreAnimation框架,通过GPU来渲染图形,节省性能,动画渲染直接提交给手机GPU,不消耗内存。 每个CAShapeLayer对象都代表着将要被渲染到屏幕上的一个任意的形状(shape),具体的形状由其path(类型为CGPathRef)属性指定。 普通的CALayer是矩形,需要frame属性。CAShapeLayer初始化时也需要指定frame值,但它本身没有形状,它的形状来源于其属性path ,CAShapeLayer中shape需要形状才能生效。UIBezierPath可以为其提供绘制形状的path。

UIBezierPath常用属性

@property(nonatomic) CGPathRef CGPath;

属性描述Core Graphics的路径表示,此属性包含路径在任何给定时间点的快照,返回一个不可变的CGPathRef对象,可以将该对象传递给Core Graphics函数。CGPathRef对象本身是由UIBezierPath对象拥有的,它只在UIBezierPath进一步变化之前有效,因为在设置新路径时,此方法将复制提供的路径。

@property(nonatomic,readonly) CGPoint currentPoint;

属性描述图形路径中的当前点。此属性中的值表示一条新的线段或曲线段的起点(上一条线段或曲线段的终点)。如果路径当前为空,则此属性值为CGPointZero。

UIBezierPath常用函数

+ (instancetype)bezierPath;

函数描述 : 创建并返回一个新的UIBezierPath对象。

返回值 : 一个新的空路径对象。

+ (instancetype)bezierPathWithRect:(CGRect)rect;

属性描述创建并返回一个用矩形路径初始化的新UIBezierPath对象。此方法通过从rect的原点开始并按顺时针方向(相对于默认坐标系统)添加线段来创建闭合的子路径。

参数 :

rect : 描述要创建的路径的矩形。

返回值 :一个带有矩形路径的新路径对象。

\color{red}{例如 :快速的创建一个矩形的贝塞尔曲线}

- (void)viewDidLoad {
    [super viewDidLoad];
    UIView *testView = [[UIView alloc]initWithFrame:CGRectMake(CGRectGetWidth(self.view.frame) / 2 - 50, CGRectGetHeight(self.view.frame) / 2 - 50, 100, 100)];
    [self.view addSubview:testView];
    [self addShapeLayer:testView];
}

/// 添加形状层
/// @param view 添加形状层的视图
- (void)addShapeLayer:(UIView *)view {
    UIBezierPath *path = [UIBezierPath bezierPathWithRect:view.bounds];
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.path = path.CGPath;
    [view.layer addSublayer:shapeLayer];
}
截屏2022-06-30 17.25.03.png
+ (instancetype)bezierPathWithOvalInRect:(CGRect)rect;

函数描述创建并返回一个新的UIBezierPath对象,该对象初始化时使用指定矩形内接的椭圆路径。该方法使用贝塞尔曲线序列创建一个接近椭圆的闭合子路径。路径是按顺时针方向创建的(相对于默认坐标系统)。如果rect参数指定一个正方形,则内接路径为圆。

参数 :

rect : 画椭圆的长方形。

返回值 : 一个带有椭圆路径的新路径对象。

\color{red}{例如 :快速的创建一个椭圆的贝塞尔曲线}

- (void)viewDidLoad {
    [super viewDidLoad];
    UIView *testView = [[UIView alloc]initWithFrame:CGRectMake(CGRectGetWidth(self.view.frame) / 2 - 100, CGRectGetHeight(self.view.frame) / 2 - 50, 200, 100)];
    [self.view addSubview:testView];
    [self addShapeLayer:testView];
}

/// 添加形状层
/// @param view 添加形状层的视图
- (void)addShapeLayer:(UIView *)view {
    UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:view.bounds];
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.path = path.CGPath;
    [view.layer addSublayer:shapeLayer];
}
截屏2022-06-30 17.28.31.png
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii;

函数描述创建并返回一个新的UIBezierPath对象,该对象初始化为一个圆角矩形路径。此方法创建一个闭合的子路径,在创建必要的线段和曲线段时,按顺时针方向(相对于默认坐标系统)进行。

参数 :

rect : 定义路径的基本形状的矩形。

corners : 位掩码值,用于标识要处理得到圆角的角。可以使用此参数只对矩形的一部分角进行圆角处理。

cornerRadii : 每个角的半径为椭圆形。大于矩形宽度或高度一半的值被适当地钳制为宽度或高度的一半。

返回值 : 一个带有圆角的矩形路径的新路径对象。

//矩形圆角位置位掩码
typedef NS_OPTIONS(NSUInteger, UIRectCorner) {
    //左上
    UIRectCornerTopLeft     = 1 << 0,
    //右上
    UIRectCornerTopRight    = 1 << 1,
    //左下
    UIRectCornerBottomLeft  = 1 << 2,
    //右下
    UIRectCornerBottomRight = 1 << 3,
    //所有角
    UIRectCornerAllCorners  = ~0UL
};

\color{red}{例如 :快速的创建一个带有指定圆角的矩形贝塞尔曲线}

- (void)viewDidLoad {
    [super viewDidLoad];
    UIView *testView = [[UIView alloc]initWithFrame:CGRectMake(CGRectGetWidth(self.view.frame) / 2 - 100, CGRectGetHeight(self.view.frame) / 2 - 50, 200, 100)];
    [self.view addSubview:testView];
    [self addShapeLayer:testView];
}

/// 添加形状层
/// @param view 添加形状层的视图
- (void)addShapeLayer:(UIView *)view {
    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight cornerRadii:CGSizeMake(15, 15)];
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.path = path.CGPath;
    [view.layer addSublayer:shapeLayer];
}

截屏2022-07-01 10.29.55.png
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;

函数描述创建并返回一个新的用圆弧初始化的UIBezierPath对象,此方法创建打开的子路径(路径起点与终点不会闭合)。创建的圆弧位于指定圆的周长上,例如在默认坐标系中绘制时,指定0弧度的开始角,π弧度的结束角,并将顺时针参数设置为“YES”将绘制圆的下半部分。但是指定相同的开始和结束角度,但将顺时针参数设置为“NO”将绘制圆的上半部分。调用此方法后,将当前点(currentPoint属性)设置为圆弧上圆的结束角处的点(终点)。

参数 :

center : 指定用于定义圆弧的圆的圆心(在当前坐标系中)。

radius : 指定用于定义圆弧的圆的半径。

startAngle : 指定圆弧的起始角度(以弧度度量)。

endAngle :指定圆弧的结束角度(以弧度度量)。

clockwise : 画弧的方向。YES为自起点到终点顺时针画 NO为自起点到终点逆时针画。

返回值 :具有指定弧的新路径对象。

\color{red}{例如 :快速的创建一个圆弧的贝塞尔曲线}

- (void)viewDidLoad {
    [super viewDidLoad];
    UIView *testView = [[UIView alloc]initWithFrame:CGRectMake(CGRectGetWidth(self.view.frame) / 2 - 100, CGRectGetHeight(self.view.frame) / 2 - 50, 200, 100)];
    [self.view addSubview:testView];
    [self addShapeLayer:testView];
}

/// 添加形状层
/// @param view 添加形状层的视图
- (void)addShapeLayer:(UIView *)view {
    UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(view.bounds.size.width / 2,view.bounds.size.height) radius:view.bounds.size.height / 2 startAngle:M_PI * 2 endAngle:M_PI  clockwise:NO];
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.path = path.CGPath;
    [view.layer addSublayer:shapeLayer];
}
截屏2022-07-01 11.45.12.png
- (void)moveToPoint:(CGPoint)point;

函数描述将调用方的当前点(currentPoint属性)移动到指定位置,此方法隐式地结束当前子路径(如果有的话),并将当前点(currentPoint属性)设置为point参数中的值。在结束前一个子路径时,此方法实际上并不关闭子路径,因此前一个子路径的第一个和最后一个点彼此不连接。对于许多路径操作,必须在发出导致绘制直线或曲线段的任何命令之前调用此方法

参数 :

point : 在当前坐标系中的一个点。

- (void)addLineToPoint:(CGPoint)point;

函数描述向调用方的路径追加一条直线。此方法创建一个从当前点(currentPoint属性)开始并在point参数指定的点结束的直线段。在添加线段之后,此方法将当前点(currentPoint属性)更新为point参数中的值。在调用此方法之前,必须设置路径的当前点(使用moveToPoint:方法或通过先前创建的直线或曲线段)。如果路径为空,则此方法不执行任何操作。

参数 :

point : 在当前坐标系中指定的线段的终点。

- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2;

函数描述 : 此方法将一条进行了三次曲折的贝塞尔曲线附加到路径。在调用此方法之前,必须设置路径的当前点(使用 moveToPoint: 方法或通过先前创建的直线或曲线段),添加曲线段后,此方法将当前点(currentPoint属性)更新为线段中终点(endPoint)的值。如果路径为nil,则此方法不执行任何操作。网上很好的记录了曲率涉及所有点之间的复杂数学关系。

参数 :

endPoint : 曲线的终点。

controlPoint1 : 计算曲线曲率时使用的第一个控制点。

controlPoint2 : 计算曲线曲率时使用的第二个控制点。

\color{red}{例如可以便捷的添加这种曲线:}

- (void)viewDidLoad {
    [super viewDidLoad];
    UIBezierPath *path = [UIBezierPath bezierPath];
    [path moveToPoint: CGPointMake(CGRectGetMinX(self.view.frame), CGRectGetMidY(self.view.frame))];
    [path addCurveToPoint:CGPointMake(CGRectGetMaxX(self.view.frame), CGRectGetMidY(self.view.frame)) controlPoint1:CGPointMake(CGRectGetMidX(self.view.frame) / 2, CGRectGetMidY(self.view.frame) + 200) controlPoint2:CGPointMake(CGRectGetMidX(self.view.frame) + CGRectGetMidX(self.view.frame) / 2,CGRectGetMidY(self.view.frame) - 200)];
    
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.fillColor = nil;
    shapeLayer.lineWidth = 2.5f;
    shapeLayer.strokeColor = [UIColor blueColor].CGColor;
    shapeLayer.path = path.CGPath;
    [self.view.layer addSublayer:shapeLayer];
}
截屏2022-07-21 17.07.39.png
- (void)addArcWithCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise API_AVAILABLE(ios(4.0));

函数描述在调用方的路径上附加一个弧,此方法添加从当前点(currentPoint属性)开始的指定弧,创建的弧位于指定圆的周长上。例如指定一个角0开始弧度,π结束角弧度,将顺时针参数设置为YES画圆的下半部分。但是如果指定相同的开始和结束角,而将顺时针参数设置为NO,则会绘制圆的上半部分。调用此方法后,将当前点(currentPoint属性)设置为圆弧上圆角端点处的点(终点)。

参数 :

center : 指定用于定义圆弧的圆(在当前坐标系中)的中心点。

radius : 指定用于定义弧的圆的半径。

startAngle :指定弧的起始角(以弧度度量)。

endAngle :指定弧的结束角(以弧度度量)。

clockwise : 画弧的方向。

- (void)closePath;

函数描述关闭最近添加的子路径。此方法通过在子路径的第一个点和最后一个点之间创建线段来关闭当前子路径。此方法随后将当前点(currentPoint属性)更新到新创建的线段的末尾,这也是现在关闭的子路径中的第一个点(闭环了)。

- (void)removeAllPoints;

函数描述从调用方中删除所有点,从而有效地删除所有子路径。

- (void)appendPath:(UIBezierPath *)bezierPath;

函数描述将指定路径对象的内容附加到调用方的路径。此方法用于将UIBezierPath创建的形状路径添加到调用方路径的末端。此方法不会显式地尝试连接两个对象中的子路径,尽管bezierPath中的操作仍然可能导致这种效果。

参数 :

bezierPath : 要添加到调用方的路径。

- (void)fillWithBlendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;

函数描述使用指定的混合模式和透明度值绘制由调用方路径包围的区域。此方法使用当前填充颜色和图形属性(加上指定的混合模式和透明度值)填充路径。如果路径包含任何打开的子路径,此方法在绘制填充区域之前隐式关闭它们。

绘制区域包括路径线本身之前的像素,但不包括路径线本身。对于具有较大线宽的路径,这可能导致填充区域和描边路径(其本身以路径线为中心)之间的重叠。此方法在绘制之前自动保存当前图形状态,并在绘制完成后恢复该状态,因此不必自己保存图形状态。

参数 :

blendMode : 混合模式决定了填充路径如何与任何现有的渲染内容组合。

alpha : 应用于填充路径的透明度量。值可以在0.0(透明)和1.0(不透明)之间。超出此范围的值被限制为0.0或1.0。

CAShapeLayer常用属性

@property(nullable) CGPathRef path;

属性描述定义要渲染的形状的路径。如果路径延伸到层边界之外,只有在正常层遮罩规则导致该情况的情况下,它才会自动裁剪到该图层。分配后将复制路径。默认为空。可设置动画。(请注意,尽管path属性是可设置动画的,但更改该属性时不会创建隐式动画。)

@property(copy) CAShapeLayerLineCap lineCap;

属性描述指定形状路径的线帽样式。线帽样式指定笔划时开放路径端点的形状。lineCap中描述了支持的值,默认值为kCALineCapButt。

  • CAShapeLayerLineCap 列举的线帽样式:
/* `lineCap' values. */
//指定笔划时开放路径的端点的对接线封口样式。
CA_EXTERN CAShapeLayerLineCap const kCALineCapButt
    API_AVAILABLE(macos(10.6), ios(3.0), watchos(2.0), tvos(9.0));
//指定笔划时开放路径的端点的圆形线帽样式。
CA_EXTERN CAShapeLayerLineCap const kCALineCapRound
    API_AVAILABLE(macos(10.6), ios(3.0), watchos(2.0), tvos(9.0));
//指定笔划时开放路径端点的方形线帽样式。
CA_EXTERN CAShapeLayerLineCap const kCALineCapSquare
    API_AVAILABLE(macos(10.6), ios(3.0), watchos(2.0), tvos(9.0));

@property(nullable, copy) NSArray<NSNumber *> *lineDashPattern;

属性描述划线时应用于形状路径的虚线图案。虚线图案指定为NSNumber对象数组,这些对象分别指定虚线图案的已绘制线段和未绘制线段的长度。例如,传递一个值为[2,3]的数组会设置一个dash模式,该模式在2个用户空间的长绘制段和3个用户空间的长未绘制段之间交替。传递值[10,5,5,5]将模式设置为一个10单元的已绘制段、一个5单元的未绘制段、一个5单元的已绘制段和一个5单元的未绘制段。默认为nil,实线。

@property CGFloat strokeStart;

属性描述开始描绘路径的相对位置,可设置动画,此属性的值必须在0.0到1.0之间,此属性的默认值为0.0。结合strokeEnd属性使用,共同定义要描绘的路径的子区域,此属性中的值指示开始描绘的路径上的相对点,而strokeEnd属性定义结束描绘的路径上的相对点,strokeStart与strokeEnd属性之间的值沿路径长度线性解释。

@property CGFloat strokeEnd;

属性描述停止描绘路径的相对位置,可设置动画,此属性的值必须在0.0到1.0之间,此属性的默认值为1.0,结合strokeStart属性使用。

@property CGFloat lineWidth;

属性描述指定形状路径的线宽。可设置动画。

@property CGRect frame;

属性描述层的矩形frame(框架)。frame是在superlayer’s的坐标空间中指定的层的位置和大小,对于层,frame矩形是一个计算属性,它是从“边界”、“锚点”和“位置”属性中的值派生出来的。为该属性指定新值时,图层将更改其“位置和边界”属性以匹配指定的矩形。矩形中每个坐标的值都是以点为单位测量的。

如果transform属性应用的旋转变换不是90度的倍数,则不要设置框架。

frame属性不是直接可动画的。相反,应该激活边界、锚点和位置属性的适当组合,以实现所需的动画结果。

@property(nullable) CGColorRef fillColor;

属性描述用于填充形状路径的颜色。可设置动画。将fillColor设置为nil将不会渲染填充。默认为不透明黑色。

@property(nullable) CGColorRef strokeColor;

属性描述用于绘制形状路径的颜色。可设置动画。将strokeColor设置为nil将不会呈现绘制形状。默认是nil。

@property(copy) CAShapeLayerFillRule fillRule;

属性描述填充形状路径时使用的填充规则。可能的值显示在形状填充模式值中。默认值为kCAFillRuleNonZero。

贝塞尔曲线画圆与弧度具参考价值的图片(红色标注只代表大体所在位置,不代表精度):

circular.png

测试代码

\color{red}{比例视图动画:}

//
//  CircleScaleView.h

#import <UIKit/UIKit.h>



@interface CircleScaleView : UIView

@property (nonatomic,assign)float maxValue;//最大值
@property (nonatomic,assign)float currentValue;//当前值

@end

//
//  CircleScaleView.m

#import "CircleScaleView.h"

@interface CircleScaleView()<CAAnimationDelegate>

@end

@implementation CircleScaleView

- (void)drawRect:(CGRect)rect {
    //背景圆环的贝塞尔曲线
    UIBezierPath *backgroundPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.bounds.size.width / 2,self.bounds.size.height / 2) radius:self.bounds.size.height / 2 - 9 / 2 startAngle:(3*M_PI) / 4  endAngle:M_PI / 4 clockwise:YES];
    UIColor *storkeColor = [UIColor grayColor];
    [storkeColor setStroke];
    backgroundPath.lineWidth = 5;
    [backgroundPath stroke];

    //设置比例动画
    [self setScaleAnimation];
    //设置比例标签
    [self setScaleLabel];
}

- (void)setScaleAnimation{
    //所占比例的贝塞尔曲线
    UIBezierPath *scalePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.bounds.size.width / 2,self.bounds.size.height / 2) radius:self.bounds.size.height / 2 - 9 / 2 startAngle:(3*M_PI) / 4 endAngle:((3*M_PI) / 4 + self.currentValue/self.maxValue * 2 * M_PI) clockwise:YES];
    
    //设置形状,渲染图形
    CAShapeLayer *circleLayer = [CAShapeLayer layer];
    circleLayer.path = scalePath.CGPath;
    circleLayer.lineCap = kCALineCapRound;
    circleLayer.strokeEnd = 0.75;
    circleLayer.lineWidth = 5;
    circleLayer.frame = self.bounds;
    circleLayer.fillColor = [UIColor clearColor].CGColor;
    circleLayer.strokeColor = [UIColor greenColor].CGColor;
    [self.layer addSublayer:circleLayer];
    
    //设置动画
    CABasicAnimation *baseAnima = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    baseAnima.duration = 1.5;
    baseAnima.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
    baseAnima.delegate = self;
    baseAnima.fromValue = [NSNumber numberWithInteger:0];
    [circleLayer addAnimation:baseAnima forKey:@"strokeEndAnimation"];
}

- (void)setScaleLabel{
    //设置比例标签
    UILabel *scaleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
    scaleLabel.text = [NSString stringWithFormat:@"已领\r%d%%",(int)floor(self.currentValue / self.maxValue * 100)];
    scaleLabel.textColor = [UIColor greenColor];;
    scaleLabel.textAlignment = NSTextAlignmentCenter;
    scaleLabel.font = [UIFont systemFontOfSize:12];
    scaleLabel.numberOfLines = 0;
    [self addSubview:scaleLabel];
    [scaleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.center.equalTo(self);
    }];
}

@end

//
//  BezierPathController.h


#import <UIKit/UIKit.h>


@interface BezierPathController : UIViewController

@end

//
//  BezierPathController.m


#import "BezierPathController.h"
#import "CircleScaleView.h"

@interface BezierPathController ()

@end

@implementation BezierPathController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    CircleScaleView *circleView = [[CircleScaleView alloc] initWithFrame:CGRectZero];
    circleView.backgroundColor = [UIColor whiteColor];
    circleView.maxValue = 10;
    circleView.currentValue = 5;
    [self.view addSubview:circleView];
    [circleView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.center.equalTo(self.view);
        make.size.mas_equalTo(CGSizeMake(100, 100));
    }];
    
}

@end

效果如图 :

Jietu20200118-232121.gif

\color{red}{例如添加圆角 : }

//
//  CircleScaleView.m

#import "CircleScaleView.h"

@interface CircleScaleView()<CAAnimationDelegate>

@end

@implementation CircleScaleView

- (void)drawRect:(CGRect)rect {
    
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight cornerRadii:CGSizeMake(30, 30)];
    CAShapeLayer *shapLayer = [CAShapeLayer layer];
    shapLayer.frame = self.bounds;
    shapLayer.path = maskPath.CGPath;
    self.layer.mask = shapLayer;
}

@end
//
//  BezierPathController.m

#import "BezierPathController.h"
#import "CircleScaleView.h"

@interface BezierPathController ()

@end

@implementation BezierPathController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    CircleScaleView *circleView = [[CircleScaleView alloc] initWithFrame:CGRectZero];
    circleView.backgroundColor = [UIColor redColor];
    circleView.maxValue = 10;
    circleView.currentValue = 5;
    [self.view addSubview:circleView];
    [circleView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.center.equalTo(self.view);
        make.size.mas_equalTo(CGSizeMake(300, 300));
    }];
    
}

@end

效果如图:

截屏2020-01-23下午1.08.45.png

\color{red}{例如为带边框的标签添加三个圆角:}

//
//  BezierPathController.m


#import "BezierPathController.h"
#import "CircleScaleView.h"

@interface BezierPathController ()

@end

@implementation BezierPathController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = @"贝塞尔曲线图";
    
    CircleScaleView *circleView = [[CircleScaleView alloc] initWithFrame:CGRectZero];
    circleView.backgroundColor = [UIColor redColor];
    circleView.maxValue = 10;
    circleView.currentValue = 5;
    [self.view addSubview:circleView];
    [circleView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.center.equalTo(self.view);
        make.size.mas_equalTo(CGSizeMake(300, 300));
    }];
    
    UILabel *testLabel = [[UILabel alloc]initWithFrame:CGRectZero];
    testLabel.text = @"贝塞尔曲线图";
    testLabel.font = [UIFont systemFontOfSize:15];
    testLabel.textColor = [UIColor redColor];
    testLabel.textAlignment = NSTextAlignmentCenter;
    [self.view addSubview:testLabel];
    [testLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.bottom.equalTo(circleView.mas_top).offset(- 60);
        make.width.mas_equalTo([self calculateTheSizeOfTheText:@"贝塞尔曲线图"].width + 20);
        make.height.mas_equalTo([self calculateTheSizeOfTheText:@"贝塞尔曲线图"].height + 20);
        make.centerX.equalTo(self.view);
    }];
    
    [self.view layoutIfNeeded];
    [self drawOuterBorder:testLabel];
    
}

///返回单个字符的高度
- (CGSize)singleCharactorSizeWithFont:(UIFont *)font {
    NSString *text = @"C";
    return [text sizeWithAttributes:@{NSFontAttributeName: font}];
}

///计算文本的的大小
- (CGSize)calculateTheSizeOfTheText:(NSString *)text{
    CGSize itemSize = CGSizeZero;
    itemSize.height = [self singleCharactorSizeWithFont:[UIFont systemFontOfSize:15]].height;
    CGSize size = CGSizeMake (CGFLOAT_MAX,itemSize.height);
    CGRect rect = [text boundingRectWithSize:size options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:15]} context:nil];
    itemSize = rect.size;
    return itemSize;
}

///为视图添加指定位置圆角
- (void)viewAddCornerRadius:(UIView *)view applyRoundCorners:(UIRectCorner)corners radius:(CGFloat)radius{
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:corners cornerRadii:CGSizeMake(radius, radius)];
    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    maskLayer.frame = view.bounds;
    maskLayer.path = maskPath.CGPath;
    view.layer.mask = maskLayer;
}


///绘制外边框
- (void)drawOuterBorder:(UIView *)view{
    view.layer.borderColor = [UIColor greenColor].CGColor;
    view.layer.borderWidth = 2.0;
    [self viewAddCornerRadius:view applyRoundCorners:UIRectCornerTopLeft | UIRectCornerTopRight | UIRectCornerBottomRight radius:([self calculateTheSizeOfTheText:@"贝塞尔曲线图"].height + 20) / 2];
}

效果如图,并不理想:

截屏2020-02-27下午11.17.41.png

可以改为不直接在标签上设置边框,而是由CAShapeLayer绘制出来,例如:

//
//  BezierPathController.m


#import "BezierPathController.h"
#import "CircleScaleView.h"

@interface BezierPathController ()

@end

@implementation BezierPathController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = @"贝塞尔曲线图";
    
    CircleScaleView *circleView = [[CircleScaleView alloc] initWithFrame:CGRectZero];
    circleView.backgroundColor = [UIColor redColor];
    circleView.maxValue = 10;
    circleView.currentValue = 5;
    [self.view addSubview:circleView];
    [circleView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.center.equalTo(self.view);
        make.size.mas_equalTo(CGSizeMake(300, 300));
    }];
    
    UILabel *testLabel = [[UILabel alloc]initWithFrame:CGRectZero];
    testLabel.text = @"贝塞尔曲线图";
    testLabel.font = [UIFont systemFontOfSize:15];
    testLabel.textColor = [UIColor redColor];
    testLabel.textAlignment = NSTextAlignmentCenter;
    [self.view addSubview:testLabel];
    [testLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.bottom.equalTo(circleView.mas_top).offset(- 60);
        make.width.mas_equalTo([self calculateTheSizeOfTheText:@"贝塞尔曲线图"].width + 20);
        make.height.mas_equalTo([self calculateTheSizeOfTheText:@"贝塞尔曲线图"].height + 20);
        make.centerX.equalTo(self.view);
    }];
    
    [self.view layoutIfNeeded];
    [self drawOuterBorder:testLabel];
    
}

///返回单个字符的高度
- (CGSize)singleCharactorSizeWithFont:(UIFont *)font {
    NSString *text = @"C";
    return [text sizeWithAttributes:@{NSFontAttributeName: font}];
}

///计算文本的的大小
- (CGSize)calculateTheSizeOfTheText:(NSString *)text{
    CGSize itemSize = CGSizeZero;
    itemSize.height = [self singleCharactorSizeWithFont:[UIFont systemFontOfSize:15]].height;
    CGSize size = CGSizeMake (CGFLOAT_MAX,itemSize.height);
    CGRect rect = [text boundingRectWithSize:size options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:15]} context:nil];
    itemSize = rect.size;
    return itemSize;
}

///为视图添加指定位置圆角
- (void)viewAddCornerRadius:(UIView *)view applyRoundCorners:(UIRectCorner)corners radius:(CGFloat)radius{
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:corners cornerRadii:CGSizeMake(radius, radius)];
    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    maskLayer.frame = view.bounds;
    maskLayer.path = maskPath.CGPath;
    view.layer.mask = maskLayer;
}

///为视图添加指定位置的圆角
- (void)viewAddCornerRadius2:(UIView *)view applyRoundCorners:(UIRectCorner)corners radius:(CGFloat)radius{
    UIBezierPath *outerBorderPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight | UIRectCornerBottomRight cornerRadii:CGSizeMake(radius, radius)];
    CAShapeLayer *borderLayer = [CAShapeLayer layer];
    borderLayer.path = outerBorderPath.CGPath;
    borderLayer.lineWidth = 2.0;
    borderLayer.frame = view.bounds;
    borderLayer.fillColor = [UIColor clearColor].CGColor;
    borderLayer.strokeColor = [UIColor greenColor].CGColor;
    [view.layer addSublayer:borderLayer];
}

///绘制外边框
- (void)drawOuterBorder:(UIView *)view{
    [self viewAddCornerRadius2:view applyRoundCorners:UIRectCornerTopLeft | UIRectCornerTopRight | UIRectCornerBottomRight radius:([self calculateTheSizeOfTheText:@"贝塞尔曲线图"].height + 20) / 2];
}

效果如图 :

截屏2020-02-27下午11.44.14.png

\color{red}{例如为视图添加虚线边框:}

//
//  BezierPathController.m


#import "BezierPathController.h"
#import "CircleScaleView.h"

@interface BezierPathController ()

@end

@implementation BezierPathController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    CircleScaleView *circleView = [[CircleScaleView alloc] initWithFrame:CGRectZero];
    circleView.backgroundColor = [UIColor redColor];
    circleView.maxValue = 10;
    circleView.currentValue = 5;
    [self.view addSubview:circleView];
    [circleView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.center.equalTo(self.view);
        make.size.mas_equalTo(CGSizeMake(300, 300));
    }];
    [self.view layoutIfNeeded];
    [self addDottedLineBorderWithView:circleView LineWidth:5 lineMargin:10 lineLength:5 lineColor:[UIColor greenColor]];
    
}

/**
*  给视图添加虚线边框
*
*  @param lineWidth  线宽
*  @param lineMargin 每条虚线之间的间距
*  @param lineLength 每条虚线的长度
*  @param lineColor 每条虚线的颜色
*/

- (void)addDottedLineBorderWithView:(UIView *)view LineWidth:(CGFloat)lineWidth lineMargin:(CGFloat)lineMargin lineLength:(CGFloat)lineLength lineColor:(UIColor *)lineColor;
{
    CAShapeLayer *border = [CAShapeLayer layer];
    
    border.strokeColor = lineColor.CGColor;
    
    border.fillColor = nil;
    
    border.path = [UIBezierPath bezierPathWithRect:view.bounds].CGPath;
    
    border.frame = view.bounds;
    
    border.lineWidth = lineWidth;
    
    border.lineCap = @"round";
    
    border.lineDashPattern = @[@(lineLength), @(lineMargin)];
    
    [view.layer addSublayer:border];
}


@end

效果如图 :

截屏2020-01-24下午3.46.04.png

\color{red}{例如按钮单圈的扩散 : }

- (void)viewDidLoad {
    
    [super viewDidLoad];
    self.navigationItem.title = @"测试代码控制器";
    
    UIButton *testCodeButton = [UIButton buttonWithType:UIButtonTypeCustom];
    testCodeButton.frame = CGRectMake(CGRectGetMaxX(self.view.frame) / 2 - 70 / 2, CGRectGetMaxY(self.view.frame) / 2 - 30 / 2, 70, 70);
    testCodeButton.backgroundColor = [UIColor blueColor];
    testCodeButton.titleLabel.font = [UIFont systemFontOfSize:15];
    [testCodeButton setTitle:@"测试代码" forState:UIControlStateNormal];
    [testCodeButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    testCodeButton.layer.cornerRadius = 35;
    [self.view addSubview:testCodeButton];
    [self addAnimation:testCodeButton];

}

///添加动画
- (void)addAnimation:(UIButton *)sender{
    //创建并返回一个新的UIBezierPath对象,该对象初始化时使用指定矩形内接的椭圆路径
    UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(sender.frame.origin.x, sender.frame.origin.y, sender.frame.size.width, sender.frame.size.height)];
    //初始化形状层CAShapeLayer
    CAShapeLayer *senderLayer = [[CAShapeLayer alloc] init];
    //图层中心点在其父图层坐标空间中的位置
    senderLayer.position = CGPointMake(sender.frame.origin.x + sender.frame.size.width / 2, sender.frame.origin.y + sender.frame.size.height / 2);
    //层的边界矩形
    senderLayer.bounds = CGRectMake(sender.frame.origin.x, sender.frame.origin.y, sender.frame.size.width, sender.frame.size.height);
    //层的背景颜色
    senderLayer.backgroundColor = [UIColor clearColor].CGColor;
    //指定形状路径的线宽
    senderLayer.lineWidth = 5;
    //用于绘制形状路径的颜色
    senderLayer.strokeColor = [UIColor blueColor].CGColor;
    //用于填充形状路径的颜色
    senderLayer.fillColor = [UIColor clearColor].CGColor;
    //定义要渲染的形状的路径
    senderLayer.path = path.CGPath;
    //在已属于接收器的其他子层下插入指定的子层
    [self.view.layer insertSublayer:senderLayer below:sender.layer];
    //结束时的矩形
    CGRect endRect = CGRectInset(CGRectMake(sender.frame.origin.x, sender.frame.origin.y, sender.frame.size.height, sender.frame.size.height), -20, -20);
    //根据结束时的矩形创建结束时UIBezierPath对象
    UIBezierPath *endPath = [UIBezierPath bezierPathWithOvalInRect:endRect];
    //要渲染的形状的路径
    senderLayer.path = endPath.CGPath;
    //接收器的不透明度,默认值为1.0
    senderLayer.opacity = 0.0;
    //贝塞尔的开始到结束位置的动画
    CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
    pathAnimation.fromValue = (__bridge id _Nullable)(path.CGPath);
    pathAnimation.toValue = (__bridge id _Nullable)(endPath.CGPath);
    //动画时长
    pathAnimation.duration = 1.0;
    //动画重复次数
    pathAnimation.repeatCount = HUGE_VALF;
    //贝塞尔的开始到结束透明度的动画
    CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
    opacityAnimation.fromValue = [NSNumber numberWithFloat:0.6];
    opacityAnimation.toValue = [NSNumber numberWithFloat:0.0];
    //动画时长
    opacityAnimation.duration = 1.0;
    //动画重复次数
    opacityAnimation.repeatCount = HUGE_VALF;
    
    [senderLayer addAnimation:opacityAnimation forKey:@""];
    [senderLayer addAnimation:pathAnimation forKey:@"path"];
    
}

效果如图 :

Jietu20200310-111004.gif

\color{red}{例如:简单的泡泡View:}

@interface UIPaopaoView : UIView

@end

@implementation UIPaopaoView{
    CGFloat kArrowHeight; //箭头高度
    CGFloat kArrowWidth;  //箭头宽度
    CGFloat kCornerRadius; //圆角半径
    CGFloat kArrowPosition; //参与箭头起点计算的值
    CGFloat kCornerRadiusArrowPadding;//箭头与圆角间距
}

- (instancetype)initWithFrame:(CGRect)frame{
    self = [super initWithFrame:frame];
    if(self){
        //箭头高度
        kArrowHeight = 8.0;
        //箭头宽度
        kArrowWidth = 15.0;
        //圆角
        kCornerRadius = 10.0;
        //箭头与圆角间距
        kCornerRadiusArrowPadding = 10.0;
        //箭头位置居左
        kArrowPosition = kCornerRadius + kCornerRadiusArrowPadding;
        //箭头位置居中
        //kArrowPosition = 0.5 * self.frame.size.width - 0.5 * kArrowWidth;
        //箭头位置居右
        //kArrowPosition = self.frame.size.width - kCornerRadius - kArrowWidth - kCornerRadiusArrowPadding;
        
        //设置阴影颜色
        self.layer.shadowColor = [UIColor colorWithRed:0.0 green:0.0 blue:1.0 alpha:1.0].CGColor;
        //设置阴影不透明度
        self.layer.shadowOpacity = 0.5;
        //设置阴影偏移量
        self.layer.shadowOffset = CGSizeMake(0, 0);
        //设置阴影圆角
        self.layer.shadowRadius = 2.0;
        
        //泡泡
        UIView *paopao = [[UIView alloc] initWithFrame: self.bounds];
        paopao.backgroundColor = [UIColor whiteColor];
        paopao.layer.cornerRadius = 3.0;
        paopao.layer.masksToBounds = YES;
        paopao.layer.mask = [self drawPaoPaoViewMaskLayer:paopao];
        
        [self addSubview:paopao];
    }
    return self;
}

///绘制遮罩层
- (CAShapeLayer *)drawPaoPaoViewMaskLayer:(UIView *)paopaoView
{
    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    maskLayer.frame = paopaoView.bounds;
    //右上弧中心
    CGPoint topRightArcCenter = CGPointMake(self.frame.size.width - kCornerRadius, kArrowHeight + kCornerRadius);
    //左上弧中心
    CGPoint topLeftArcCenter = CGPointMake(kCornerRadius, kArrowHeight + kCornerRadius);
    //右下弧中心
    CGPoint bottomRightArcCenter = CGPointMake(self.frame.size.width - kCornerRadius, self.
                                               frame.size.height- kArrowHeight - kCornerRadius);
    //左下弧中心
    CGPoint bottomLeftArcCenter = CGPointMake(kCornerRadius, self.frame.size.height - kArrowHeight - kCornerRadius);

    UIBezierPath *path = [UIBezierPath bezierPath];
    //将接收器的当前点移动到指定位置
    [path moveToPoint: CGPointMake(0, kArrowHeight + kCornerRadius)];
    //向接收器的路径追加一条直线(左侧直线)
    [path addLineToPoint: CGPointMake(0, bottomLeftArcCenter.y)];
    //添加左下角弧线
    [path addArcWithCenter: bottomLeftArcCenter radius: kCornerRadius startAngle: -M_PI endAngle: -M_PI-M_PI_2 clockwise: NO];
    //指向下的箭头
    [path addLineToPoint: CGPointMake(kArrowPosition, self.frame.size.height - kArrowHeight)];
    [path addLineToPoint: CGPointMake(kArrowPosition + 0.5 * kArrowWidth, self.frame.size.height)];
    [path addLineToPoint: CGPointMake(kArrowPosition+kArrowWidth, self.frame.size.height - kArrowHeight)];
    [path addLineToPoint: CGPointMake(self.frame.size.width - kCornerRadius, self.frame.size.height - kArrowHeight)];

    //添加右下角弧线
    [path addArcWithCenter: bottomRightArcCenter radius: kCornerRadius startAngle: -M_PI-M_PI_2 endAngle: -M_PI * 2 clockwise: NO];
    //向接收器的路径追加一条直线(右侧直线)
    [path addLineToPoint: CGPointMake(self.frame.size.width, kArrowHeight+kCornerRadius)];
    //添加右上角弧线
    [path addArcWithCenter: topRightArcCenter radius: kCornerRadius startAngle: 0 endAngle: -M_PI_2 clockwise: NO];
    //指向上的箭头
    //[path addLineToPoint: CGPointMake(kArrowPosition + kArrowWidth, kArrowHeight)];
    //[path addLineToPoint: CGPointMake(kArrowPosition + 0.5 * kArrowWidth, 0)];
    //[path addLineToPoint: CGPointMake(kArrowPosition, kArrowHeight)];
    //[path addLineToPoint: CGPointMake(kCornerRadius, kArrowHeight)];
    
    //添加左上角弧线
    [path addArcWithCenter: topLeftArcCenter radius: kCornerRadius startAngle: -M_PI_2 endAngle: -M_PI clockwise: NO];
    //关闭路径
    [path closePath];

    maskLayer.path = path.CGPath;

    return maskLayer;
}

@end

@interface BezierPathController ()

@end

@implementation BezierPathController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = @"贝塞尔曲线图";
    UIPaopaoView *paopaoView = [[UIPaopaoView alloc]initWithFrame:CGRectMake(CGRectGetMaxX(self.view.frame) / 2 - 150 / 2, CGRectGetMaxY(self.view.frame) / 2 - 150 / 2, 150, 75)];
    [self.view addSubview:paopaoView];
}

@end

样式如图:

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

推荐阅读更多精彩内容