iOS~ 贝塞尔曲线:UIBezierPath和CAShapeLayer

ios ~ 贝塞尔UIBezierPath+ CAShapeLa

2022-09-23  本文已影响0人  阳光下的叶子呵

画一张饼图,并且带动画效果点击效果
如下图所示:

RPReplay_Final1663921137.GIF
如图,
白色分割线是layerlineWidth
动画效果:是加了一个遮罩.mask,在下面就会有实现代码;
点击效果:是点击的是哪一个扇形图,就将它的半径加长。

相关链接:1、iOS 饼状图(扇形图)动画效果的实现,还可以在扇形图上添加UILabel文字
2、iOS 带指示线说明的饼状图
3、Touch:判断当前点击的位置是否在某个视图上
4、CALayer之mask属性-遮罩

下面是实现代码:

@interface GW_CareerHomeHeaderView ()

/** 扇形图:大小同一个中心点 */
@property (nonatomic, strong) UIView    *fanChartFatherView; // 扇形图的父view
@property (nonatomic, strong) CAShapeLayer *big_FanChart_FatherLayer;   // 大扇形图 父layer:点击小扇形图时,放大
@property (nonatomic, strong) CAShapeLayer *small_FanChart_FatherLayer; // (遮罩层)

/// 点击扇形图,在其位置上添加一个(相同角度和颜色)半径放大的扇形图(每次点击删掉重新创建)
//@property (nonatomic, strong) CAShapeLayer *selectedIndex_Layer; // 新创建覆盖在当前点击的layer上,视觉效果有点不太好,不推荐这种方法
/// 扇形图,之前形态是否是“放大版”(大:选中状态, 小:非选中状态):YES 当前是“放大版”,NO 当前是“缩小版”
@property (nonatomic, assign) BOOL previous_is_selected_FanChart_Big;
@property (nonatomic, assign) NSInteger previous_SelextedIndex_fanChart; // 上一次选中的是第几个

@property (nonatomic, strong) NSArray   *statistical_TypeArray;

@end

1、首先,根据数据,创建扇形图(饼图)、遮罩层、创建完成再添加动画效果:
#pragma mark --  类型分布 扇形图 --
/// 当统计没有数据时,改变高度,并显示缺省图
- (void)refreshChangeStatistical {
    
    // 数据所占百分比:5%,10%,15%,20%,50%
    self.statistical_TypeArray = @[
        @"5",@"15", @"10",@"20", @"50"
    ];
    
    // 饼图(点击某一个扇形图,放大效果):

    // 首先,在创建之前,先将可能已有的删除掉:
    [self.fanChartFatherView removeFromSuperview];
    [self.big_FanChart_FatherLayer removeFromSuperlayer];
    [self.small_FanChart_FatherLayer removeFromSuperlayer];
    
    _fanChartFatherView = [[UIView alloc] initWithFrame:CGRectMake([UIScreen mainScreen].bounds.size.width/375*13, [UIScreen mainScreen].bounds.size.width/375*(457 - 13 - 173), [UIScreen mainScreen].bounds.size.width/375*173, [UIScreen mainScreen].bounds.size.width/375*173)];
    self.fanChartFatherView.backgroundColor = RGBA(218, 255, 251, 1);
//    self.fanChartFatherView.backgroundColor = UIColor.whiteColor;
    [self.backView addSubview:self.fanChartFatherView];
    
    /// 贝塞尔曲线(父layer,每次用时removeFromSuperlayer删除一下)
    /// 大(扇形图和mask遮罩层的父layer):
    _big_FanChart_FatherLayer = [[CAShapeLayer alloc] init];
    _big_FanChart_FatherLayer.backgroundColor = [UIColor whiteColor].CGColor;
    UIBezierPath *big_BackbezierPath = [UIBezierPath
                                    bezierPathWithRoundedRect:CGRectMake(0, 0, self.fanChartFatherView.frame.size.width, self.fanChartFatherView.frame.size.height)
                                    byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight
                                    cornerRadii:CGSizeMake([UIScreen mainScreen].bounds.size.width/375*0, [UIScreen mainScreen].bounds.size.width/375*0)];

    _big_FanChart_FatherLayer.lineWidth = [UIScreen mainScreen].bounds.size.width/375*0.01;
    // 颜色
    _big_FanChart_FatherLayer.strokeColor = [UIColor clearColor].CGColor;
    // 背景填充色
    _big_FanChart_FatherLayer.fillColor = [UIColor clearColor].CGColor;
    _big_FanChart_FatherLayer.path = [big_BackbezierPath CGPath];
    
    [self.fanChartFatherView.layer addSublayer:self.big_FanChart_FatherLayer];
    
    
    /** 准备画扇形图: */
    // 扇形中心点
    CGFloat centerX = self.fanChartFatherView.frame.size.width * 0.5f;
    CGFloat centerY = self.fanChartFatherView.frame.size.height * 0.5f;
    CGPoint centerPoint = CGPointMake(centerX, centerY);
    CGFloat big_radius = [UIScreen mainScreen].bounds.size.width/375*(85); // 大半径
    CGFloat small_radius = [UIScreen mainScreen].bounds.size.width/375*(77); // 小半径
    
    // 大 饼图:(遮罩层):radius 是1/2的半径长度,(在这里的是大半径big_radius的原因是:之后,点击某一个扇形图之后,该扇形图的半径会变大显示,但变大之后的半径不会超过big_radius)
    UIBezierPath *bigFanChartPath = [UIBezierPath bezierPathWithArcCenter:centerPoint
                                                          radius:big_radius/2
                                                      startAngle:-M_PI_2
                                                        endAngle:M_PI_2 * 3
                                                       clockwise:YES];
    
    _small_FanChart_FatherLayer = [CAShapeLayer layer];
    // 填充色
    _small_FanChart_FatherLayer.fillColor   = [UIColor clearColor].CGColor;
    // 线条颜色
    _small_FanChart_FatherLayer.strokeColor = [UIColor whiteColor].CGColor;
    // 线条宽度
    _small_FanChart_FatherLayer.lineWidth   = big_radius;
    _small_FanChart_FatherLayer.strokeStart = 0.0f;
    _small_FanChart_FatherLayer.strokeEnd   = 1.0f;
    _small_FanChart_FatherLayer.zPosition   = 1;
    _small_FanChart_FatherLayer.path        = [bigFanChartPath CGPath];
//    _small_FanChart_FatherLayer.backgroundColor = RGBA(0, 0, 0, 1).CGColor;
    // 遮罩
    self.big_FanChart_FatherLayer.mask = self.small_FanChart_FatherLayer;
    
    
    
    // 计算每个类型所占总数的百分比:
    CGFloat fanChart_total = 0.0f;
    for (int i = 0; i < self.statistical_TypeArray.count; i++) {
        fanChart_total = fanChart_total + [self.statistical_TypeArray[i] floatValue];
    }
    
    CGFloat small_start = 0.0f;
    CGFloat small_end = 0.0f;

    for (int i = 0; i < self.statistical_TypeArray.count; i++) {
        // 计算当前end位置 = 上一个结束位置 + 当前部分百分比 =(当前部分结束为止的百分比+上一次结束的百分比)
        small_end = [self.statistical_TypeArray[i] floatValue] / fanChart_total + small_start;
        
        NSLog(@"😁😁small_start = %f, small_end = %f", small_start, small_end);
        
        //图层
        CAShapeLayer *subLayer = [CAShapeLayer layer];
        subLayer.backgroundColor = RGBA(0, 0, 0, 1).CGColor;
        
        // 背景填充色(每个数据一个颜色)
        if (i == 0) {
            subLayer.fillColor = RGBA(226, 73, 85, 1).CGColor;
        } else if (i == 1) {
            subLayer.fillColor = RGBA(255, 196, 15, 1).CGColor;
        } else if (i == 2) {
            subLayer.fillColor = RGBA(153, 153, 153, 1).CGColor;
        } else if (i == 3) {
            subLayer.fillColor = RGBA(60, 134, 196, 1).CGColor;
        } else if (i == 4) {
            subLayer.fillColor = RGBA(33, 63, 111, 1).CGColor;
        }
        else {
            subLayer.fillColor = UIColor.blackColor.CGColor;
        }
        
        // 线条的颜色
        subLayer.strokeColor = UIColor.whiteColor.CGColor;
        // 线宽
        subLayer.lineWidth   = [UIScreen mainScreen].bounds.size.width/375*2;
        // 在z轴上的位置 支持隐式动画
        subLayer.zPosition   = 2;
        subLayer.lineJoin = kCALineJoinBevel; // 尖角 kCALineJoinMiter, 圆角 kCALineJoinRound, 缺角 kCALineJoinBevel
        //subLayer.lineCap = kCALineCapRound; // 无端点 kCALineCapButt,圆形端点 kCALineCapRound,方形端点 kCALineCapSquare
        // 注意📢:下边的贝塞尔曲线,已经设置好start和end了,这里不在需要设置(如果设置下边strokeStart和strokeEnd的话,lineWidth显示出问题)
        //subLayer.strokeStart = small_start;
        //subLayer.strokeEnd = small_end;
        
        
        // 初始化一个路径:创建圆弧 ,startAngle:起始点,endAngle:终止点,clockwise:顺时针方向 ,M_PI == π:3.1415926
        // clockwise 顺时针 YES, 逆时针 NO, (M_PI_2 = π/2) = 270°
//        UIBezierPath *small_FanChartPath = [UIBezierPath bezierPathWithArcCenter:centerPoint
//                                                              radius:small_radius
//                                                          startAngle:-M_PI_2 + M_PI*2*small_start
//                                                            endAngle:-M_PI_2 + M_PI*2*small_end
//                                                           clockwise:YES];
//        [small_FanChartPath closePath];

//        // 每个扇形 同角度方向,偏移5,造成有间隔的形状
//        CGFloat centerAngle = M_PI * (big_start + big_end);
//        CGFloat centerPointX = 5 * sinf(centerAngle) + centerX;
//        CGFloat centerPointY = -5 * cosf(centerAngle) + centerY;
//        CGPoint centerPoint1 = CGPointMake(centerPointX, centerPointY);
        
        UIBezierPath *small_FanChartPath = [UIBezierPath bezierPath];
//        [small_FanChartPath moveToPoint:centerPoint]; // 设置closePath后,就不必设置moveToPoint了
        [small_FanChartPath addArcWithCenter:centerPoint
                                   radius:small_radius
                               startAngle:-M_PI_2 + M_PI*2*small_start
                                 endAngle:-M_PI_2 + M_PI*2*small_end
                                clockwise:YES];
        [small_FanChartPath addLineToPoint:centerPoint];
        
        // 闭合路径,即在终点和起点连一根线
        [small_FanChartPath closePath];
        
        // 将UIBezierPath类转换成CGPath,类似于UIColor的CGColor
        subLayer.path = [small_FanChartPath CGPath];
        [self.big_FanChart_FatherLayer addSublayer:subLayer];
//        [self.fanChartFatherView.layer addSublayer:subLayer];
                
        // 计算下一个start位置 = 当前end位置
        small_start = small_end;
    }
    
    [self stroke];
}

- (void)stroke {
    //画图动画
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    animation.duration  = 3;
    animation.fromValue = @0.0f;
    animation.toValue   = @1.0f;
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    animation.removedOnCompletion = YES;
    [self.small_FanChart_FatherLayer addAnimation:animation forKey:@"circleAnimation"];
}
2、饼图中扇形点击事件

CAShapeLayer *subLayr = self.big_FanChart_FatherLayer.sublayers[i];
选中某一个扇形图,改变其半径的长短,实现放大缩小的效果

#pragma mark -- 饼图中扇形点击事件方法 --
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    
    UITouch *touch = touches.anyObject;
//    CGPoint point = [touch locationInView:self.fanChartFatherView];
    
    CGPoint point = [touch preciseLocationInView:self.fanChartFatherView];
    
    // 如果矩形不为null,或空,并且该点位于矩形内,返回YES,在范围外面 返回NO
    if (CGRectContainsPoint(self.fanChartFatherView.bounds, point)) {
//        NSLog(@"😆😆 结束触摸屏幕 手指所在 view上的位置 point.X ==== %f,\n point.Y ==== %f", point.x, point.y);
        
        NSInteger selectIndex = [self getCurrentSelectedOneTouch:point];
        
        if (selectIndex >= 0) { //点击第几个扇形图,那个扇形图就改变其半径长度
            
            // 扇形中心点
            CGFloat centerX = self.fanChartFatherView.frame.size.width * 0.5f;
            CGFloat centerY = self.fanChartFatherView.frame.size.height * 0.5f;
            CGPoint centerPoint = CGPointMake(centerX, centerY);
            CGFloat big_radius = [UIScreen mainScreen].bounds.size.width/375*(85); // 大半径
            CGFloat small_radius = [UIScreen mainScreen].bounds.size.width/375*(77); // 小半径
            
            // 计算每个类型所占总数的百分比:
            CGFloat fanChart_total = 0.0f;
            for (int i = 0; i < self.statistical_TypeArray.count; i++) {
                fanChart_total = fanChart_total + [self.statistical_TypeArray[i] floatValue];
            }
            
            CGFloat small_start = 0.0f;
            CGFloat small_end = 0.0f;
            
            
            // [self.selectedIndex_Layer removeFromSuperlayer]; // 新创建覆盖在当前点击的layer上,视觉效果有点不太好,不推荐这种方法

            for (int i = 0; i < self.statistical_TypeArray.count; i++) {
                // 计算当前end位置 = 上一个结束位置 + 当前部分百分比 =(当前部分结束为止的百分比+上一次结束的百分比)
                small_end = [self.statistical_TypeArray[i] floatValue] / fanChart_total + small_start;
                
                CAShapeLayer *subLayr = self.big_FanChart_FatherLayer.sublayers[i];
                
                /** 选中某一个扇形图,改变其半径的长短,实现放大缩小的效果 */
                if (i == selectIndex) {
                    
                    if (selectIndex == self.previous_SelextedIndex_fanChart) {
                        
                        if (self.previous_is_selected_FanChart_Big == YES) {
                            
                            UIBezierPath *small_FanChartPath = [UIBezierPath bezierPath];
                            [small_FanChartPath addArcWithCenter:centerPoint
                                                       radius:small_radius
                                                   startAngle:-M_PI_2 + M_PI*2*small_start
                                                     endAngle:-M_PI_2 + M_PI*2*small_end
                                                    clockwise:YES];
                            [small_FanChartPath addLineToPoint:centerPoint];
                            // 闭合路径,即在终点和起点连一根线
                            [small_FanChartPath closePath];
                            subLayr.path = [small_FanChartPath CGPath];
                            
                            self.previous_is_selected_FanChart_Big = NO;
                            
                        } else {
                            UIBezierPath *small_FanChartPath = [UIBezierPath bezierPath];
                            [small_FanChartPath addArcWithCenter:centerPoint
                                                       radius:big_radius
                                                   startAngle:-M_PI_2 + M_PI*2*small_start
                                                     endAngle:-M_PI_2 + M_PI*2*small_end
                                                    clockwise:YES];
                            [small_FanChartPath addLineToPoint:centerPoint];
                            // 闭合路径,即在终点和起点连一根线
                            [small_FanChartPath closePath];
                            subLayr.path = [small_FanChartPath CGPath];
                            
                            self.previous_is_selected_FanChart_Big = YES;
                        }
                        
                    } else {
                        UIBezierPath *small_FanChartPath = [UIBezierPath bezierPath];
                        [small_FanChartPath addArcWithCenter:centerPoint
                                                   radius:big_radius
                                               startAngle:-M_PI_2 + M_PI*2*small_start
                                                 endAngle:-M_PI_2 + M_PI*2*small_end
                                                clockwise:YES];
                        [small_FanChartPath addLineToPoint:centerPoint];
                        // 闭合路径,即在终点和起点连一根线
                        [small_FanChartPath closePath];
                        subLayr.path = [small_FanChartPath CGPath];
                        
                        self.previous_is_selected_FanChart_Big = YES;
                    }
                    
                    // 更新选中的是第几个
                    self.previous_SelextedIndex_fanChart = selectIndex;
                    
                } else {
                    UIBezierPath *small_FanChartPath = [UIBezierPath bezierPath];
                    [small_FanChartPath addArcWithCenter:centerPoint
                                               radius:small_radius
                                           startAngle:-M_PI_2 + M_PI*2*small_start
                                             endAngle:-M_PI_2 + M_PI*2*small_end
                                            clockwise:YES];
                    [small_FanChartPath addLineToPoint:centerPoint];
                    // 闭合路径,即在终点和起点连一根线
                    [small_FanChartPath closePath];
                    subLayr.path = [small_FanChartPath CGPath];
                }
                
                
                /**
                 
                 /// 新创建覆盖在当前点击的layer上,视觉效果有点不太好,不推荐这种方法
                if (i == selectIndex) {

                    //图层
                    _selectedIndex_Layer = [CAShapeLayer layer];
                    _selectedIndex_Layer.backgroundColor = RGBA(0, 0, 0, 1).CGColor;

                    // 背景填充色
                    if (i == 0) {
                        _selectedIndex_Layer.fillColor = RGBA(226, 73, 85, 1).CGColor;
                    } else if (i == 1) {
                        _selectedIndex_Layer.fillColor = RGBA(255, 196, 15, 1).CGColor;
                    } else if (i == 2) {
                        _selectedIndex_Layer.fillColor = RGBA(153, 153, 153, 1).CGColor;
                    } else if (i == 3) {
                        _selectedIndex_Layer.fillColor = RGBA(60, 134, 196, 1).CGColor;
                    } else if (i == 4) {
                        _selectedIndex_Layer.fillColor = RGBA(33, 63, 111, 1).CGColor;
                    } else {
                        _selectedIndex_Layer.fillColor = UIColor.blackColor.CGColor;
                    }

                    // 线条的颜色
                    _selectedIndex_Layer.strokeColor = UIColor.whiteColor.CGColor;
                    // 线宽
                    _selectedIndex_Layer.lineWidth   = [UIScreen mainScreen].bounds.size.width/375*2;
                    // 在z轴上的位置 支持隐式动画
                    _selectedIndex_Layer.zPosition   = 2;
                    _selectedIndex_Layer.lineJoin = kCALineJoinBevel; // 尖角 kCALineJoinMiter, 圆角 kCALineJoinRound, 缺角 kCALineJoinBevel

                    UIBezierPath *small_FanChartPath = [UIBezierPath bezierPath];
                    [small_FanChartPath addArcWithCenter:centerPoint
                                               radius:big_radius
                                           startAngle:-M_PI_2 + M_PI*2*small_start
                                             endAngle:-M_PI_2 + M_PI*2*small_end
                                            clockwise:YES];
                    [small_FanChartPath addLineToPoint:centerPoint];

                    // 闭合路径,即在终点和起点连一根线
                    [small_FanChartPath closePath];

                    // 将UIBezierPath类转换成CGPath,类似于UIColor的CGColor
                    _selectedIndex_Layer.path = [small_FanChartPath CGPath];
                    [self.big_FanChart_FatherLayer addSublayer:self.selectedIndex_Layer];
                
                }
                 */

                // 计算下一个start位置 = 当前end位置
                small_start = small_end;
            }
            [self.big_FanChart_FatherLayer display];
        }
        
    }
    
}

/// 点击的区域,在第几个扇形图的范围,返回点击的第一个扇形
- (NSInteger)getCurrentSelectedOneTouch:(CGPoint)point {
    
    __block NSInteger selectIndex = -1;
    
    // 坐标重置
    CGAffineTransform transform = CGAffineTransformIdentity;
    
    // 遍历饼图中的子layer(self.big_FanChart_FatherLayer 是扇形图layer的父layer)
    [self.big_FanChart_FatherLayer.sublayers enumerateObjectsUsingBlock:^(__kindof CALayer * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        
        CAShapeLayer *shapeLayer = (CAShapeLayer *)obj;
        CGPathRef path = [shapeLayer path];
        
        // CGPathContainsPoint:如果触摸的点包含在路径内,则返回YES
        if (CGPathContainsPoint(path, &transform, point, 0)) {
            selectIndex = idx;
            NSLog(@"👍🏻👍🏻点击的是第几个扇形图 = %ld", selectIndex);
        }
    }];
    
    return selectIndex;
}

上一篇 下一篇

猜你喜欢

热点阅读