CoreGraphics,CoreAnimation实战, 可交互动画图表

96
作者 Syik
2017.09.10 20:07 字数 1013

前言

图表的绘制相信大家都用的很多, 也有现成的很好的框架, 但如果定制程度特别高, 特别是动画, 还是得自己来实现, 先看看准备实现的效果, 个人觉得还是有一些炫酷的.

另外本文不会科普最基本的概念与Api, 直接从实战出发, 希望大家看完后都能写出各种炫酷的效果

曲线图




曲线图在平时用的应该是最多的, 曲线图会了, 折线图就更容易了.

图上的效果大致分3步(下面的动画也一样):

1.处理数据: 将得到的数据转换为点坐标数据, 这一步就不细说了

2.绘制图形: 可以用Quartz2D或者UIKit中封装好的UIBezierPath

3.设置动画: 主要利用到CoreAnimation中的"strokeEnd"动画

下面就看具体代码吧:

绘制图形
/*
 pointArray是所有点的数组
 color是主题色
 compete绘制完成的回调
*/
- (void)drawLayerWithPointArray:(NSMutableArray *)pointArray color:(UIColor *)color compete:(completeBlock)compete{
    
    //初始化下面渐变色路径
    UIBezierPath *fillPath = [UIBezierPath new];
    //初始化曲线的路径
    UIBezierPath *borderPath = [UIBezierPath new];
    
    //这里是我个人设置点数过多 忽略部分点, 让曲线更平滑, 按需删除
    NSInteger ignoreSpace = pointArray.count / 15;
    
    //记录上一个点
    __block CGPoint lastPoint;
    //记录上一个点的索引
    __block NSUInteger  lastIdx;
    //渐变色路径移动到左下角
    [fillPath moveToPoint:CGPointMake(0, _chart.height)];
    //遍历所有点, 移动Path绘制图形
    [pointArray enumerateObjectsUsingBlock:^(NSValue *obj, NSUInteger idx, BOOL * _Nonnull stop) {
        
        CGPoint point = obj.CGPointValue;
        
        if (idx == 0) { //第一个点
            
            [fillPath addLineToPoint:point];
            [borderPath moveToPoint:point];
            lastPoint = point;
            lastIdx = idx;
        } else if ((idx == pointArray.count - 1) || (point.y == 0) || (lastIdx + ignoreSpace + 1 == idx)) { //最后一个点最高点要画/当点数过多时 忽略部分点
            
            [fillPath addCurveToPoint:point controlPoint1:CGPointMake((lastPoint.x + point.x) / 2, lastPoint.y) controlPoint2:CGPointMake((lastPoint.x + point.x) / 2, point.y)]; //三次曲线
            [borderPath addCurveToPoint:point controlPoint1:CGPointMake((lastPoint.x + point.x) / 2, lastPoint.y) controlPoint2:CGPointMake((lastPoint.x + point.x) / 2, point.y)];
            lastPoint = point;
            lastIdx = idx;
        }
    }];
    //将渐变色区域封闭
    [fillPath addLineToPoint:CGPointMake(_chart.width, _chart.height)];
    [fillPath addLineToPoint:CGPointMake(0, _chart.height)];
    
    //初始化Path的载体分别显示路径及填充渐变色
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.path = fillPath.CGPath;
    [_chart.layer addSublayer:shapeLayer];
    
    CAShapeLayer *borderShapeLayer = [CAShapeLayer layer];
    borderShapeLayer.path = borderPath.CGPath;
    borderShapeLayer.lineWidth = 2.f;
    borderShapeLayer.strokeColor = color.CGColor;
    borderShapeLayer.fillColor = [UIColor clearColor].CGColor;
    [_chart.layer addSublayer:borderShapeLayer];
    
    //设置渐变色
    CAGradientLayer *gradientLayer = [CAGradientLayer layer];
    gradientLayer.frame = _chart.bounds;
    [gradientLayer setColors:[NSArray arrayWithObjects:(id)[[color colorWithAlphaComponent:0.5] CGColor], (id)[[UIColor clearColor] CGColor], nil]];
    [gradientLayer setStartPoint:CGPointMake(0.5, 0)];
    [gradientLayer setEndPoint:CGPointMake(0.5, 1)];
    [gradientLayer setMask:shapeLayer];
    [_chart.layer addSublayer:gradientLayer];
    
    compete(borderShapeLayer, shapeLayer, gradientLayer);
}

以上 一个曲线图就画完了, 下面看看怎么样让它动起来

设置动画
- (void)animation{
    //动画之前让曲线不隐藏
    _bulletBorderLayer.hidden = NO;
    
    //路径动画的KeyPath为@"strokeEnd"
    //根据需要的效果, 从0-1意味着画完整个曲线
    CABasicAnimation *animation1 = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    animation1.fromValue = @(0);
    animation1.toValue = @(1);
    animation1.duration = 0.8;
    
    [_bulletBorderLayer addAnimation:animation1 forKey:nil];
    //动画需要0.8秒完成, 延迟0.8秒让渐变色动画, 当然也可以用代理
    [self performSelector:@selector(bulletLayerAnimation) withObject:nil afterDelay:0.8];
}


- (void)bulletLayerAnimation{
    //动画之前让渐变色不隐藏  
    _bulletLayer.hidden = NO;
    
    //渐变色看起来像是从上往下长出来, 实际只是透明度的变化
    CABasicAnimation *animation2 = [CABasicAnimation animationWithKeyPath:@"opacity"];
    animation2.fromValue = @(0);
    animation2.toValue = @(1);
    animation2.duration = 0.4;
    
    [_bulletLayer addAnimation:animation2 forKey:nil];
}

整个曲线图效果就完成了.

柱状图


柱状图其实更容易, 只是绘制这种柱状图稍微麻烦一点点而已,
这里我没有用strokeEnd, 而是直接垂直方向高度变化, 需要注意的是图表的Y方向跟屏幕坐标系的Y方向是相仿的, 所以这里是位置动画加上垂直方向缩放动画的组动画, 也就是AnimationGroup

绘制图形
/*
 wordsArrayRandom是乱序过后的词语数组, 记录了每个词语的频次
*/
CGFloat maxHeight = _chart.height; //确定最大高度
CGFloat width = 2; //确定竖线宽度
CGFloat margin = _chart.width / 9;
NSInteger maxCount = wordsModel.count.integerValue;
[wordsArrayRandom enumerateObjectsUsingBlock:^(BAWordsModel *wordsModel, NSUInteger idx, BOOL * _Nonnull stop) {
    
    //绘制
    CGPoint orginPoint = CGPointMake(margin * idx, maxHeight); //圆点, 在矩形下边中间
    CGFloat height = maxHeight * wordsModel.count.integerValue / maxCount; //高度
    
    //其实就是一个矩形加上一个圆形
    UIBezierPath *path = [UIBezierPath new];
    [path moveToPoint:orginPoint];
    [path addLineToPoint:CGPointMake(path.currentPoint.x - width / 2, path.currentPoint.y)];
    [path addLineToPoint:CGPointMake(path.currentPoint.x, path.currentPoint.y - height)];
    [path addLineToPoint:CGPointMake(path.currentPoint.x + width, path.currentPoint.y)];
    [path addLineToPoint:CGPointMake(path.currentPoint.x, orginPoint.y)];
    [path addLineToPoint:orginPoint];
    [path addArcWithCenter:CGPointMake(orginPoint.x, maxHeight - height) radius:width * 2 startAngle:0 endAngle:M_PI * 2 clockwise:YES];
    
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.path = path.CGPath;
    shapeLayer.hidden = YES;
    shapeLayer.fillColor = [BAWhiteColor colorWithAlphaComponent:0.8].CGColor;
    [_chart.layer addSublayer:shapeLayer];
    
    [_barLayerArray addObject:shapeLayer];
}];

绘制的代码我摘出了比较重要的部分, 全部的大家可以去下载Demo查看

设置动画
//每间隔0.1秒, 动画一个柱状图
- (void)animation{
    for (NSInteger i = 0; i < 10; i++) {
        CAShapeLayer *layer = _barLayerArray[9 - i];
        [self performSelector:@selector(animateLayer:) withObject:layer afterDelay:i * 0.1];
    }
}

- (void)animateLayer:(CAShapeLayer *)layer{
    
    layer.hidden = NO;
    
    //垂直方向的缩放
    CABasicAnimation *animation1 = [CABasicAnimation animationWithKeyPath:@"transform.scale.y"];
    animation1.fromValue = @(0.0);
    animation1.toValue = @(1.0);
    
    //同时垂直方向坐标原点在变化
    CABasicAnimation *animation2 = [CABasicAnimation animationWithKeyPath:@"transform.translation.y"];
    animation2.fromValue = @(_chart.height);
    animation2.toValue = @(0.0);
    
    CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
    animationGroup.duration = 0.3;
    animationGroup.animations = @[animation1, animation2];
    
    [layer addAnimation:animationGroup forKey:nil];
}

柱状图也完成了, 上面的都还好, 最后看看饼状图的绘制吧

饼状图


我们看到饼状图不光是需要绘制/动画/还需要一个交互.
我们都知道CAShapeLayer是也是Layer, 本身是并不响应用户点击的, 所以这里需要手动处理, 还是一步步来说.

绘制图形

本身绘制饼状图不复杂, 但是要绘制连接线和小图标就麻烦了一点, 另外因为要整体移动饼状图, 所以每一个饼状图加上附带的图标得放到一个容器Layer里面

/*
思路: 外面的大圆跟里面小圆 其实是两条比较粗的曲线, 计算好尺寸之后拼接起来,
外面的小图标(素材)与大圆之间需要计算角度与长度才能画线连接起来
*/
- (void)drawPieChart{
    
    //设置大圆半径, 小圆半径, 中心点
    _pieRadius = self.height / 2 - 8 * BAPadding - 7;
    _inPieRadius = _pieRadius - 3 * BAPadding + 3.5;
    _pieCenter = CGPointMake(self.width / 2, self.height / 2 + 40);
    
    //外面饼状数组
    NSMutableArray *pieArray = [NSMutableArray array];
    //里面饼状数组
    NSMutableArray *inPieArray = [NSMutableArray array];
    //这个数组用来存放动画时间, 每一段的动画时间应该跟它所占比成比例
    NSMutableArray *durationArray = [NSMutableArray array];
    //容器数组, 动画时方便整体移动
    NSMutableArray *arcArray = [NSMutableArray array];
    
    //起始(终止)角度
    __block CGFloat endAngle = - M_PI / 2;
    //_giftValueArray几面已经是处理好的数据, 包括了每一块的价值
    [_giftValueArray enumerateObjectsUsingBlock:^(BAGiftValueModel *giftValueModel, NSUInteger idx, BOOL * _Nonnull stop) {
        
        //创建一个容器 放外部饼状图与内部饼状图, 为动画做准备
        CALayer *arcLayer = [CALayer layer];
        arcLayer.frame = self.bounds;
        [arcArray addObject:arcLayer];
        [self.layer addSublayer:arcLayer];
        
        //计算每个礼物的起始 终止角度
        CGFloat startAngle = endAngle;
        
        //caculateWithStartAngle是根据起始角度与最大价值算终止角度
        //_maxValue为之前计算好的总价值
        [giftValueModel caculateWithStartAngle:startAngle maxValue:_maxValue];
        endAngle = giftValueModel.endAngle;
        
        //1.2是总共的动画时间, 计算这一块动画所需要的时间
        CGFloat duration = 1.2 * giftValueModel.totalGiftValue / _maxValue;
        [durationArray addObject:@(duration)];
        
        //当前饼状图的颜色
        UIColor *pieColor = [BAWhiteColor colorWithAlphaComponent:giftValueModel.alpha];
        UIColor *inPieColor = [BAWhiteColor colorWithAlphaComponent:giftValueModel.alpha - 0.3];
        
        //画图 
        //外部饼状图路径
        UIBezierPath *piePath = [UIBezierPath bezierPath]; //内部圆环路径
        UIBezierPath *inPiePath = [UIBezierPath bezierPath];
        
        [piePath addArcWithCenter:_pieCenter radius:_pieRadius startAngle:startAngle endAngle:endAngle clockwise:YES];
        [inPiePath addArcWithCenter:_pieCenter radius:_inPieRadius startAngle:startAngle endAngle:endAngle clockwise:YES];
        
        CAShapeLayer *pieLayer = [CAShapeLayer layer];
        pieLayer.path = piePath.CGPath;
        pieLayer.lineWidth = 4 * BAPadding;
        pieLayer.strokeColor = pieColor.CGColor;
        pieLayer.fillColor = [UIColor clearColor].CGColor;
        pieLayer.hidden = YES;
        
        CAShapeLayer *inPieLayer = [CAShapeLayer layer];
        inPieLayer.path = inPiePath.CGPath;
        inPieLayer.lineWidth = 14;
        inPieLayer.strokeColor = inPieColor.CGColor;
        inPieLayer.fillColor = [UIColor clearColor].CGColor;
        inPieLayer.hidden = YES;
        
        [arcLayer addSublayer:pieLayer];
        [arcLayer addSublayer:inPieLayer];
        [pieArray addObject:pieLayer];
        [inPieArray addObject:inPieLayer];
        
        //显示各种bedge 并绘制连接线
        [self drawBedgeWithGiftValueModel:giftValueModel container:arcLayer];
    }];
    _pieArray = pieArray;
    _inPieArray = inPieArray;
    _durationArray = durationArray;
    _arcArray = arcArray;
}


- (void)drawBedgeWithGiftValueModel:(BAGiftValueModel *)giftValueModel container:(CALayer *)container{
    
    //根据不同的礼物类型显示不同的图片
    CALayer *iconLayer;
    switch (giftValueModel.giftType) {
            
        case BAGiftTypeCostGift:
            iconLayer = _costIcon;
            break;
            
        case BAGiftTypeDeserveLevel1:
            iconLayer = _deserve1Icon;
            
            break;
            
        case BAGiftTypeDeserveLevel2:
            iconLayer = _deserve2Icon;
            
            break;
            
        case BAGiftTypeDeserveLevel3:
            iconLayer = _deserve3Icon;
            
            break;
            
        case BAGiftTypeCard:
            iconLayer = _cardIcon;
            
            break;
            
        case BAGiftTypePlane:
            iconLayer = _planeIcon;
            
            break;
            
            
        case BAGiftTypeRocket:
            iconLayer = _rocketIcon;
            
            break;
            
        default:
            break;
    }
    [_bedgeArray addObject:iconLayer];
    
    CGFloat iconDistance = container.frame.size.height / 2 - 40; //图标到中心点的距离
    CGFloat iconCenterX;
    CGFloat iconCenterY;
    
    CGFloat borderDistance = _pieRadius + 2 * BAPadding;
    CGFloat lineBeginX;
    CGFloat lineBeginY;
    
    CGFloat iconBorderDistance = iconDistance - 12.5;
    CGFloat lineEndX;
    CGFloat lineEndY;
    
    CGFloat moveDistance = BAPadding; //动画移动的距离
    CGFloat moveX;
    CGFloat moveY;
    
    /*
    这里计算各种参数
    directAngle为之前计算起始终止角度时保存下来的饼状图朝向
    这个朝向需要在四个象限, 转换为锐角, 然后通过三角函数就可以算出连接线的起点终点, 图标的位置
    */
    CGFloat realDirectAngle; //锐角
    if (giftValueModel.directAngle > - M_PI / 2 && giftValueModel.directAngle < 0) { //-90° - 0°
       
        realDirectAngle = giftValueModel.directAngle - (- M_PI / 2);
        
        iconCenterX = _pieCenter.x + iconDistance * sin(realDirectAngle);
        iconCenterY = _pieCenter.y - iconDistance * cos(realDirectAngle);
        
        lineBeginX = _pieCenter.x + borderDistance * sin(realDirectAngle);
        lineBeginY = _pieCenter.y - borderDistance * cos(realDirectAngle);
        
        lineEndX = _pieCenter.x + iconBorderDistance * sin(realDirectAngle);
        lineEndY = _pieCenter.y - iconBorderDistance * cos(realDirectAngle);
        
        moveX = moveDistance * sin(realDirectAngle);
        moveY = - moveDistance * cos(realDirectAngle);
        
    } else if (giftValueModel.directAngle > 0 && giftValueModel.directAngle < M_PI / 2) { // 0° - 90°
       
        realDirectAngle = giftValueModel.directAngle;
        
        iconCenterX = _pieCenter.x + iconDistance * cos(realDirectAngle);
        iconCenterY = _pieCenter.y + iconDistance * sin(realDirectAngle);
        
        lineBeginX = _pieCenter.x + borderDistance * cos(realDirectAngle);
        lineBeginY = _pieCenter.y + borderDistance * sin(realDirectAngle);
        
        lineEndX = _pieCenter.x + iconBorderDistance * cos(realDirectAngle);
        lineEndY = _pieCenter.y + iconBorderDistance * sin(realDirectAngle);

        moveX = moveDistance * cos(realDirectAngle);
        moveY = moveDistance * sin(realDirectAngle);
        
    } else if (giftValueModel.directAngle > M_PI / 2 && giftValueModel.directAngle < M_PI) { // 90° - 180°
        
        realDirectAngle = giftValueModel.directAngle - M_PI / 2;
        
        iconCenterX = _pieCenter.x - iconDistance * sin(realDirectAngle);
        iconCenterY = _pieCenter.y + iconDistance * cos(realDirectAngle);
        
        lineBeginX = _pieCenter.x - borderDistance * sin(realDirectAngle);
        lineBeginY = _pieCenter.y + borderDistance * cos(realDirectAngle);
        
        lineEndX = _pieCenter.x - iconBorderDistance * sin(realDirectAngle);
        lineEndY = _pieCenter.y + iconBorderDistance * cos(realDirectAngle);
        
        moveX = - moveDistance * sin(realDirectAngle);
        moveY = moveDistance * cos(realDirectAngle);
        
    } else { //180° - -90°
        
        realDirectAngle = giftValueModel.directAngle - M_PI;
        
        iconCenterX = _pieCenter.x - iconDistance * cos(realDirectAngle);
        iconCenterY = _pieCenter.y - iconDistance * sin(realDirectAngle);
        
        lineBeginX = _pieCenter.x - borderDistance * cos(realDirectAngle);
        lineBeginY = _pieCenter.y - borderDistance * sin(realDirectAngle);
        
        lineEndX = _pieCenter.x - iconBorderDistance * cos(realDirectAngle);
        lineEndY = _pieCenter.y - iconBorderDistance * sin(realDirectAngle);
        
        moveX = - moveDistance * cos(realDirectAngle);
        moveY = - moveDistance * sin(realDirectAngle);
    }
    
    //画线
    UIBezierPath *linePath = [UIBezierPath bezierPath];
    [linePath moveToPoint:CGPointMake(lineBeginX, lineBeginY)];
    [linePath addLineToPoint:CGPointMake(lineEndX, lineEndY)];
    
    CAShapeLayer *lineLayer = [CAShapeLayer layer];
    lineLayer.path = linePath.CGPath;
    lineLayer.lineWidth = 1;
    lineLayer.strokeColor = [BAWhiteColor colorWithAlphaComponent:0.6].CGColor;
    lineLayer.fillColor = [UIColor clearColor].CGColor;
    lineLayer.hidden = YES;
    
    [_lineArray addObject:lineLayer];
    [container addSublayer:lineLayer];
    
    //保存移动的动画
    giftValueModel.translation = CATransform3DMakeTranslation(moveX, moveY, 0);
    
    iconLayer.frame = CGRectMake(iconCenterX - 13.75, iconCenterY - 13.75, 27.5, 27.5);
    [container addSublayer:iconLayer];
}


/**
 *  计算角度 与Y轴夹角 -90 - 270
 */
- (CGFloat)angleForStartPoint:(CGPoint)startPoint EndPoint:(CGPoint)endPoint{
    
    CGPoint Xpoint = CGPointMake(startPoint.x + 100, startPoint.y);
    
    CGFloat a = endPoint.x - startPoint.x;
    CGFloat b = endPoint.y - startPoint.y;
    CGFloat c = Xpoint.x - startPoint.x;
    CGFloat d = Xpoint.y - startPoint.y;
    
    CGFloat rads = acos(((a*c) + (b*d)) / ((sqrt(a*a + b*b)) * (sqrt(c*c + d*d))));
    
    if (startPoint.y > endPoint.y) {
        rads = -rads;
    }
    if (rads < - M_PI / 2 && rads > - M_PI) {
        rads += M_PI * 2;
    }
    
    return rads;
}

//两点之间距离
- (CGFloat)distanceForPointA:(CGPoint)pointA pointB:(CGPoint)pointB{
    CGFloat deltaX = pointB.x - pointA.x;
    CGFloat deltaY = pointB.y - pointA.y;
    return sqrt(deltaX * deltaX + deltaY * deltaY );
}

上面画整体的过程有点小复杂, 因为涉及了各种角度转换 计算, 以及为之后动画 交互做准备, 做好了前面的准备, 再进行动画跟交互处理就容易不少.

设置动画

动画的过程其实是饼状图按顺序一个个执行前面画曲线所用的strokeEnd动画, 然后我们小图标以及我们画的连接线透明度动画展现.

- (void)animation{
    NSInteger i = 0;
    CGFloat delay = 0;
    //遍历所有的饼状图, 按顺序执行动画
    for (CAShapeLayer *pieLayer in _pieArray) {
        CAShapeLayer *inPieLayer = _inPieArray[i];
        CGFloat duration = [_durationArray[i] floatValue];
        [self performSelector:@selector(animationWithAttribute:) withObject:@{@"layer" : pieLayer, @"duration" : @(duration)} afterDelay:delay inModes:@[NSRunLoopCommonModes]];
        [self performSelector:@selector(animationWithAttribute:) withObject:@{@"layer" : inPieLayer, @"duration" : @(duration)} afterDelay:delay inModes:@[NSRunLoopCommonModes]];
        delay += duration;
        i++;
    }
    
    [self performSelector:@selector(animationWithBedge) withObject:nil afterDelay:delay];
}

//根据传入的时间以及饼状图路径动画
- (void)animationWithAttribute:(NSDictionary *)attribute{
    CAShapeLayer *layer = attribute[@"layer"];
    CGFloat duration = [attribute[@"duration"] floatValue];

    layer.hidden = NO;
    
    CABasicAnimation *animation1 = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    animation1.fromValue = @(0);
    animation1.toValue = @(1);
    animation1.duration = duration;
    
    [layer addAnimation:animation1 forKey:nil];
}

//透明度渐变展示各种小图标
- (void)animationWithBedge{
    NSInteger i = 0;
    for (CAShapeLayer *lineLayer in _lineArray) {
        CALayer *bedgeLayer = _bedgeArray[i];
        
        lineLayer.hidden = NO;
        bedgeLayer.hidden = NO;
        
        CABasicAnimation *animation1 = [CABasicAnimation animationWithKeyPath:@"opacity"];
        animation1.fromValue = @(0);
        animation1.toValue = @(1);
        animation1.duration = 0.4;
        
        [lineLayer addAnimation:animation1 forKey:nil];
        [bedgeLayer addAnimation:animation1 forKey:nil];
        i++;
    }
}


处理交互

交互的思路其实很清晰, 判断一个饼状图被点击了有2个条件:

1.点击的点与圆心之间的连线与-90°(之前设定的基准)之间的夹角是否在之前计算的饼状图起始终止角度之间.
2.点击的点与圆心的距离是否大于内圆的半径(最内), 小于外圆的半径(最外).

我们发现其实这些之前已经计算好了, 所以直接计算这个点的参数

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    CGPoint touchPoint = [[touches anyObject] locationInView:self];
    
    [self dealWithTouch:touchPoint];
}


- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    CGPoint touchPoint = [[touches anyObject] locationInView:self];
    
    [self dealWithTouch:touchPoint];
}

- (void)dealWithTouch:(CGPoint)touchPoint{
    
    CGFloat touchAngle = [self angleForStartPoint:_pieCenter EndPoint:touchPoint];
    CGFloat touchDistance = [self distanceForPointA:touchPoint pointB:_pieCenter];
    //判断是否点击了鱼丸
    if (touchDistance < _inPieRadius - BAPadding) {
        
        if (self.isFishBallClicked) {
            _giftPieClicked(BAGiftTypeNone);
        } else {
            _giftPieClicked(BAGiftTypeFishBall);
        }
        [self animationFishBall];
        
        return;
    }
    
    //求点击位置与-90°的夹角 与 之前的圆弧对比
    if (touchDistance > _inPieRadius - BAPadding && touchDistance < _pieRadius + 2 * BAPadding) {
        
        [_giftValueArray enumerateObjectsUsingBlock:^(BAGiftValueModel *giftValueModel, NSUInteger idx, BOOL * _Nonnull stop) {
            
            if (giftValueModel.startAngle < touchAngle && giftValueModel.endAngle > touchAngle) {
                
                //isMovingOut用来标记是否已经移动出去了
                if (giftValueModel.isMovingOut) {
                    _giftPieClicked(BAGiftTypeNone);
                } else {
                    _giftPieClicked(giftValueModel.giftType);
                }
                
                [self animationMove:_arcArray[idx] giftValueModel:giftValueModel];
                *stop = YES;
            }
        }];
    }
}

//将传入的饼状图移动, 并且遍历所有饼状图, 联动收回之前的饼状图
- (void)animationMove:(CALayer *)arcLayer giftValueModel:(BAGiftValueModel *)giftValueModel{

    if (giftValueModel.isMovingOut) {
        arcLayer.transform = CATransform3DIdentity;
        giftValueModel.movingOut = NO;
    } else {
        arcLayer.transform = giftValueModel.translation;
        giftValueModel.movingOut = YES;
    
        [_arcArray enumerateObjectsUsingBlock:^(CALayer *arc, NSUInteger idx, BOOL * _Nonnull stop) {
            BAGiftValueModel *giftValue = _giftValueArray[idx];
            if (![arcLayer isEqual:arc] && giftValue.isMovingOut) {
                [self animationMove:arc giftValueModel:giftValue];
            }
        }];
        
        if (self.isFishBallClicked) {
            [self animationFishBall];
        }
    }
}

结语

至此, 所有炫酷的动态可交互图表就已经完成了, 其实这个App里面细节动画处理还挺多的, 例如滑动时背景渐变色的角度改变, 渐变色的动画, 包括一个有点酷的引导页, 启动页.

项目已上线: 叫直播伴侣, 可以下载下来玩玩,
另外代码也是开源的:
https://github.com/syik/BulletAnalyzer 觉得有意思的可以打赏一个Star~

项目中有一个有意思的功能, 中文语义近似的分析可以看看我的上一篇文章.

发现大家对动画更感兴趣, 下一篇讲讲动态的启动页与炫酷的引导页动画.


iOS开发笔记