preface

I believe we all use a lot of chart drawing, there are ready-made very good framework, but if the degree of customization is particularly high, especially animation, or to achieve their own, first look at the effect of the implementation, I think there are still some cool.

In addition, this article will not popularize the most basic concepts and Api, directly from the actual combat, I hope you can write all kinds of cool effects after reading

The graph


The graph should be the most used in ordinary times, the graph will be easier to break.

The effect is roughly divided into 3 steps (the same goes for the animation below):

1. Data processing: Convert the obtained data into point coordinate data, which will not be detailed in this step

2. Graph drawing: You can use the UIBezierPath encapsulated in Quartz2D or UIKit

3. Set animation: Mainly use the “strokeEnd” animation in CoreAnimation

Let’s look at the code below:

Drawing graphics
*/ - (void)drawLayerWithPointArray:(NSMutableArray *)pointArray Color :(UIColor *)color compete:(completeBlock)compete{// initialize the following gradient path UIBezierPath *fillPath = [UIBezierPath new]; UIBezierPath *borderPath = [UIBezierPath new]; NSInteger ignoreSpace = pointarray.count / 15; NSInteger ignoreSpace = pointarray.count / 15; // record the lastPoint __block CGPoint lastPoint; // Record the index of a point __block NSUInteger lastIdx; // The gradient path moves to the lower left corner [fillPath moveToPoint:CGPointMake(0, _chart.height)]; / / to iterate through all points, moving Path graph [pointArray enumerateObjectsUsingBlock: ^ (NSValue * obj, NSUInteger independence idx, BOOL * _Nonnull stop) { CGPoint point = obj.CGPointValue;if(idx == 0) {fillPath addLineToPoint:point]; [borderPath moveToPoint:point]; lastPoint = point; lastIdx = idx; }else if((independence idx = = pointArray. Count - 1) | | (point. Y = = 0) | | (lastIdx + + 1 = = ignoreSpace independence idx)) {/ / to draw the last point apogee/when points too much Ignore the part of the point [fillPath addCurveToPoint:point controlPoint1:CGPointMake((lastPoint.x + point.x) / 2, lastPoint.y) controlPoint2:CGPointMake((lastPoint.x + point.x) / 2, point.y)]; / / cubic curve [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; }}]; / / the gradient area closed [fillPath addLineToPoint: CGPointMake (_chart. Width, _chart. Height)]; [fillPath addLineToPoint:CGPointMake(0, _chart.height)]; 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]; // Set gradientLayer *gradientLayer = [CAGradientLayer layer]; gradientLayer.frame = _chart.bounds; [gradientLayersetColors: [NSArray arrayWithObjects: (id) [/ color colorWithAlphaComponent: 0.5 CGColor], (id) [[UIColor clearColor] CGColor]. nil]]; [gradientLayersetThe StartPoint: CGPointMake (0.5, 0)]; [gradientLayersetThe EndPoint: CGPointMake (0.5, 1)]; [gradientLayersetMask:shapeLayer];
    [_chart.layer addSublayer:gradientLayer];

    compete(borderShapeLayer, shapeLayer, gradientLayer);
}Copy the code

Now that I’ve drawn my graph, how do I make it work

Set the animation
- (void)animation{// Make the curve unhidden before the animation _bulletBorderLayer. Hidden = NO; // The path animation's KeyPath is @"strokeEnd"CABasicAnimation *animation1 = [CABasicAnimation animationWithKeyPath:@"strokeEnd"]; animation1.fromValue = @(0); animation1.toValue = @(1); Animation1. Duration = 0.8; [_bulletBorderLayer addAnimation:animation1forKey:nil]; // Animation takes 0.8 seconds to complete, delay 0.8 seconds for gradient animation, You can also use a proxy [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:animation2forKey:nil];
}Copy the code

The entire graph effect is complete.

A histogram

Bar diagrams are actually easier, it’s just a little bit more difficult to draw them. Instead of using strokeEnd, I’m going to change the height vertically. Notice that the Y direction of the diagram is similar to the Y direction of the screen coordinate system, so this is a group animation of position animation plus vertical scaling animation, Namely AnimationGroup

Drawing graphics
/* wordsArrayRandom is an array of words that are out of order */ CGFloat maxHeight = _chart.height; // Determine the maximum height CGFloat width = 2; CGFloat margin = _chart.width / 9; NSInteger maxCount = wordsModel.count.integerValue; [wordsArrayRandom enumerateObjectsUsingBlock:^(BAWordsModel *wordsModel, NSUInteger idx, OrginPoint = CGPointMake(margin * idx, maxHeight); / / dot, in the middle of the bottom of the rectangle 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]; }];Copy the code

Draw the code I picked out the more important part, all you can download the Demo to view

Set the animation
// Every 0.1 seconds, animate a bar graph - (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:animationGroupforKey:nil];
}Copy the code

The bar chart is done, everything above is ok, and finally let’s look at the pie chart

The pie chart

We see that pie charts need not only to be drawn/animated/but also an interaction. As we all know, CAShapeLayer is also Layer, which itself does not respond to user clicks, so we need to handle this manually, or step by step.

Drawing graphics

Drawing the pie chart itself is not complicated, but drawing the lines and small ICONS is a bit more troublesome. In addition, since the pie chart needs to be moved as a whole, each pie chart and the attached ICONS need to be placed in a container Layer

/ * : */ - (void)drawPieChart{// set the radius of the large circle, the radius of the small circle, and the radius of the small circle. _pieRadius = self.height / 2-8 * BAPadding - 7; _pieRadius = _pieRadius - 3 * BAPadding + 3.5; _pieCenter = CGPointMake(self.width / 2, self.height / 2 + 40); // pieArray = [NSMutableArray array]; // NSMutableArray *inPieArray = [NSMutableArray array]; NSMutableArray *durationArray = [NSMutableArray array]; NSMutableArray *arcArray = [NSMutableArray array]; // Start (end) Angle __block CGFloat endAngle = -m_pi / 2; / / _giftValueArray several surface have been dealing with good data, including the value of each piece of [_giftValueArray enumerateObjectsUsingBlock: ^ (BAGiftValueModel * giftValueModel, NSUInteger idx, BOOL * _Nonnull stop) {CALayer *arcLayer = [CALayer layer];  arcLayer.frame = self.bounds; [arcArray addObject:arcLayer]; [self.layer addSublayer:arcLayer]; // calculate the start and endAngle of each gift CGFloat startAngle = endAngle; / / caculateWithStartAngle is according to the starting Angle and maximum value calculation before termination Angle / / _maxValue for good value [giftValueModel caculateWithStartAngle: startAngle maxValue:_maxValue]; endAngle = giftValueModel.endAngle; / / 1.2 is total animation time, calculate the time required to this piece of animation CGFloat duration = 1.2 * giftValueModel totalGiftValue / _maxValue;  [durationArray addObject:@(duration)]; The color of the / / current pie chart UIColor * pieColor = [BAWhiteColor colorWithAlphaComponent: giftValueModel. Alpha]; UIColor *inPieColor = [BAWhiteColor colorWithAlphaComponent: giftValueModel. Alpha 0.3]; UIBezierPath *piePath = [UIBezierPath bezierPath]; // Inner ring path 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]; / / display various bedge and draw lines [self drawBedgeWithGiftValueModel: giftValueModel container: arcLayer]; }]; _pieArray = pieArray; _inPieArray =inPieArray; _durationArray = durationArray; _arcArray = arcArray; } - (void)drawBedgeWithGiftValueModel:(BAGiftValueModel *)giftValueModel container:(CALayer *)container{ // Display different images according to different gift types 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; // Distance from icon to center point 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; // Animation move distance CGFloat moveX; CGFloat moveY; DirectAngle is the direction of the pie chart saved when calculating the starting and ending angles. This direction needs to be converted into acute angles in four quadrants. Then the starting and ending points of the connecting lines can be calculated by using trigonometric functions. Icon position */ CGFloat realDirectAngle; / / acute Angleif(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 ° to 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 ° to 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]; / / save mobile animation giftValueModel. Translation = CATransform3DMakeTranslation (moveX, moveY, 0); Iconlayer. frame = CGRectMake(iconCenterx-13.75, iconCenterY -13.75, 27.5, 27.5); [container addSublayer:iconLayer]; } /** * calculate Angle with Y-axis -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;
    }

    returnrads; } // distance between two points - (CGFloat)distanceForPointA:(CGPoint)pointA pointB:(CGPoint)pointB{CGFloat deltaX = pointb.x - pointA. CGFloat deltaY = pointB.y - pointA.y;return sqrt(deltaX * deltaX + deltaY * deltaY );
}Copy the code

Drawing the whole process above is a little complicated, because it involves calculation of various Angle conversion and preparation for animation interaction. After making preparation in front, animation and interaction processing will be much easier.

Set the animation

In fact, the animation process is to execute the strokeEnd animation used to draw the curve in front of the pie chart one by one, and then our small ICONS and the transparency animation of the connection line we draw are displayed.

- (void)animation{ NSInteger i = 0; CGFloat delay = 0; // Iterate over all pie charts and animate them in orderfor (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]; } // 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]; } // The opacity gradient displays various small ICONS - (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:animation1forKey:nil];
        [bedgeLayer addAnimation:animation1 forKey:nil]; i++; }}Copy the code
Process interactions

The idea of interaction is actually very clear. There are two conditions for judging a pie chart to be clicked:

1. Whether the included Angle between the line between the click point and the center of the circle and -90°(the baseline set previously) is between the start and end angles of the pie chart calculated previously. Whether the distance between the click point and the center of the circle is greater than the radius of the inner circle (innermost) and less than the radius of the outer circle (outermost).

And we realized that this was already computed, so we just computed the parameters at this point

- (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]; // Check whether the fish balls are clickedif (touchDistance < _inPieRadius - BAPadding) {

        if (self.isFishBallClicked) {
            _giftPieClicked(BAGiftTypeNone);
        } else {
            _giftPieClicked(BAGiftTypeFishBall);
        }
        [self animationFishBall];

        return; } // Compare the Angle between the click position and -90° with the previous arcif (touchDistance > _inPieRadius - BAPadding && touchDistance < _pieRadius + 2 * BAPadding) {

        [_giftValueArray enumerateObjectsUsingBlock:^(BAGiftValueModel *giftValueModel, NSUInteger idx, BOOL * _Nonnull stop) {

            if(giftValueModel startAngle < touchAngle && giftValueModel. EndAngle > touchAngle) {/ / isMovingOut to identify whether has been moving outif (giftValueModel.isMovingOut) {
                    _giftPieClicked(BAGiftTypeNone);
                } else{ _giftPieClicked(giftValueModel.giftType); } [self animationMove:_arcArray[idx] giftValueModel:giftValueModel]; *stop = YES; }}]; }} // Move the pie chart passed in, and iterate over all the pie charts, - (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]; }}}Copy the code

conclusion

At this point, all the cool dynamic interactive charts have been completed, in fact, the App has a lot of detail animation processing, such as sliding background gradient Angle change, gradient animation, including a bit cool guide page, the launch page.

The project has been online: call live partner, you can download down to play, and the code is open source: github.com/syik/Bullet… If you find it interesting, you can get a Star

There is an interesting feature in the project. The analysis of Chinese semantic approximation can be found in my last article.

Finding that people are more interested in animation, the next article will be about dynamic boot screen and cool boot screen animation.