CoreGraphics,CoreAnimation實戰, 可互動動畫圖表

syik發表於2017-09-11

前言

圖表的繪製相信大家都用的很多, 也有現成的很好的框架, 但如果定製程度特別高, 特別是動畫, 還是得自己來實現, 先看看準備實現的效果, 個人覺得還是有一些炫酷的.

另外本文不會科普最基本的概念與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裡面細節動畫處理還挺多的, 例如滑動時背景漸變色的角度改變, 漸變色的動畫, 包括一個有點酷的引導頁, 啟動頁.

專案已上線: 叫直播伴侶, 可以下載下來玩玩,
另外程式碼也是開源的:
github.com/syik/Bullet… 覺得有意思的可以打賞一個Star~

專案中有一個有意思的功能, 中文語義近似的分析可以看看我的上一篇文章.

發現大家對動畫更感興趣, 下一篇講講動態的啟動頁與炫酷的引導頁動畫.

相關文章