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

前言

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

另外本文不會科普最基本的概念與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~

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容

  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,151評論 4 61
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,613評論 25 708
  • DFS(Depth-First-Search)深度優先算法,是搜索算法的一種。是一種在開發爬蟲早期使用較多的方法。...
    Jiao123閱讀 5,490評論 3 7
  • 我的故鄉,是回不去的地方,到不了的遠方…… 曾想過,若是沒有走出大山,走出那個小小村莊,終歸是種遺憾。然而真的走出...
    吾念所歸閱讀 248評論 2 1
  • 你不要離開我,我沒有辦法像愛你那樣去愛其他人了。有時候我們對一段感情死不放手我們自己都分不清是因為太愛了還是因為不...
    還是會笑閱讀 600評論 0 0