前言
此本中收錄一些較復雜統計圖案例的實現分析,希望能給需要的朋友帶來靈感。
曲線動態圖
繪制關鍵步驟:
我們可以看到上圖的動圖是一組組合動畫,共有四部分組成:坐標橫豎虛線的動畫、曲線的動態繪制、小圓點的動畫、漸變區域的動畫。下面逐個分析
-
坐標橫豎虛線的動畫
第一步設置一個 CAShapeLayer 并設置 .lineDashPattern 屬性,使之成為虛線。下面一步很關鍵,生成一條 UIBezierPath,使用for循環如下:for (NSInteger idx = 0; idx < horizontalLineCount; ++idx) { yValue = [self.parentView.datasource lineChartView:self.parentView valueReferToHorizontalReferenceLineAtIndex:idx]; yLocation = [self.parentView.calculator verticalLocationForValue:yValue]; [horizontalReferencePath moveToPoint:CGPointMake(0, yLocation)]; [horizontalReferencePath addLineToPoint:CGPointMake(boundsWidth, yLocation)]; }
通過多次調用 moveToPoint,addLineToPoint,于是這條UIBezierPath就包含了三段直線,把UIBezierPath 賦值給CAShapeLayer后,直接對 CAShapeLayer的strokeEnd 作CABasicAnimation動畫,就會出現,三條橫線依次出現的動畫,很巧妙,而不是你看到的初始化三條UIBezierPath。同時對橫豎方向的CAShapeLayer做動畫,就會出現如圖所示的效果。
曲線動畫
這部分的重點是使用 貝塞爾曲線的拼接曲線的方法:
addCurveToPoint 三次貝塞爾曲線,需要兩個控制點
addQuadCurveToPoint 二次貝塞爾曲線,需要一個控制點
關鍵是根據數值,計算出各個控制點,調用繪圖方法繪制曲線路徑。最后對CAShapeLayer的strokeEnd 作CABasicAnimation動畫即可實現。-
小圓點的動畫
根據數據源,在每一數據點處放上一個自定義UIView,在此自定義UIView的drawRect中繪制圓形圖形,并且設置 shape.layer.opacity = 0;,即讓這些小圓點(很多UIView)剛開始的是不顯示的,加載在當前的UIView上,計算每一個點的動畫開始時間,達到小圓點依次作動畫的效果。對每一個圓點調用下面方法:- (void)addScaleSpringAnimationForView:(UIView *)view reverse:(BOOL)isReverse delay:(CGFloat)delay forKeyPath:(NSString *)keyPath { CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"]; animation.keyTimes = !isReverse ? @[@0.05, @0.5, @0.9] : @[@0.5, @0.9]; animation.values = !isReverse ? @[@0.01, @2.5, @1.0] : @[@2.5, @0.01]; CABasicAnimation *baseAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; baseAnimation.fromValue = isReverse ? @1.0 : @0.5; baseAnimation.toValue = isReverse ? @0.5 : @1.0; CAAnimationGroup *groundAnimation = [[CAAnimationGroup alloc] init]; groundAnimation.duration = 0.5; groundAnimation.speed = 0.5; groundAnimation.animations = @[animation, baseAnimation]; groundAnimation.delegate = self; groundAnimation.beginTime = CACurrentMediaTime() + delay; [groundAnimation setValue:view.layer forKey:keyPath]; //******************** [view.layer addAnimation:groundAnimation forKey:nil]; } - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { CALayer *layer = [anim valueForKeyPath:@"original"]; if (layer) { layer.opacity = 1.0; [anim setValue:nil forKeyPath:@"original"]; } }
我們注意到,上面的動畫是一個組合動畫,讓圓點透明度變大,形狀先變大后變小的動畫。這里要注意一點的是為了使用動畫的代理,區分動畫,我們使用了
[groundAnimation setValue:view.layer forKey:keyPath];
因為最開始時小圓點是不顯示的,但是動畫結束后我們需要它顯示,所以在動畫的代理里 設置動畫的 layer.opacity = 1.0;使其一直顯示。
漸變區域的動畫
我們仔細觀察上圖會發現,漸變區域的動畫是這樣的,先慢慢變清晰,同時波浪往上移動的效果,它是怎樣實現的呢?
首先我們設置一個漸變圖層 CAGradientLayer,下面是CAGradientLayer基本介紹
CAGradientLayer可以方便的處理顏色漸變,它有以下幾個主要的屬性:
@property(copy) NSArray *colors 漸變顏色的數組
@property(copy) NSArray *locations 漸變顏色的區間分布,locations的數組長度和color一致,默認是nil,會平均分布。
@property CGPoint startPoint 映射locations中第一個位置,用單位向量表示,比如(0,0)表示從左上角開始變化。默認值是(0.5,0.0)。
@property CGPoint endPoint 映射locations中最后一個位置,用單位向量表示,比如(1,1)表示到右下角變化結束。默認值是(0.5,1.0)。
我們本例中的設置是這樣的
gradientLayer.colors = @[[UIColor colorWithWhite:1.0 alpha:0.9], [UIColor colorWithWhite:1.0 alpha:0.0]];
gradientLayer.locations = @[@(0.0), @(0.95)];
因為 漸變圖層默認是從上到下均勻渲染的,此處的設置的意思是頂部的是 透明度為0.9的白色
底部0.95的地方開始是透明度為0的白色,
# 整個設置的意思是說,底部0.5比例處開始向上顏色漸變,并且是越來越白,頂部的白是0.9透明度的白色。
設置漸變圖層的 mask(遮罩層)為一個CAShapeLayer
maskLayer = [CAShapeLayer layer];
maskLayer.strokeColor = [UIColor clearColor].CGColor;
maskLayer.fillColor = [UIColor blackColor].CGColor;
gradientLayer.mask = maskLayer;
我們在上面繪制曲線路徑的時候已經得到一個UIBezierPath,把這個路徑拼接上X坐標軸上的兩個垂直投影點形成一個底部矩形狀的封閉路徑,把個路徑作為漸變圖層的path,并繪制一條比這個UIBezierPath頂部低一點的路徑作為 漸變圖層的遮罩圖層(maskLayer)的路徑 LowBezierPath。
//animation for gradient
animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
animation.duration = _animationDuration;
animation.speed = 1.5;
animation.fromValue = @0.0;
animation.toValue = @1.0;
costarAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
costarAnimation.duration = _animationDuration;
costarAnimation.speed = 1.5;
costarAnimation.fromValue = (__bridge id _Nullable)(gradientPathLower.CGPath);
costarAnimation.toValue = (__bridge id _Nullable)(gradientPath.CGPath);
//********************
[gradientLayer addAnimation:animation forKey:nil];
[maskLayer addAnimation:costarAnimation forKey:nil];
對漸變圖層和漸變圖層的 遮罩層同時做CABasicAnimation動畫,漸變圖層漸漸顯現,漸變圖層的遮罩圖層由 低路徑過渡到高路徑,就有了上圖中漸變圖層漸漸顯現并逐漸身高的效果。
在使用drawRect:重繪頁面時注意首先移除已有的圖層maskLayer 同時做動畫。
- (void)drawRect:(CGRect)rect {
//remove sublayer
[[self subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
[[self.layer sublayers] makeObjectsPerformSelector:@selector(removeFromSuperlayer)];
帶彈性的曲線圖
整個效果的實現過程是這樣的:
-
觸發UIView的 drawRect 方法;
[_lineGraph setNeedsDisplay]; **使用 setNeedsDisplay **
在 drawRect 中 對小白點的動畫延遲到 x 秒后,彈性動畫開始的延遲時間為 0秒持續 x秒,這樣就可以保證在彈性動畫結束后,開始小白點的動畫。
-
彈性動畫效果
if (!_displayLink) { _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleWaveFrameUpdate)]; [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; _displayLink.paused = true; } __block UIView *shape; CGFloat middleY = (firstPoint.y + lastPoint.y) / 2; NSMutableArray *controlPoints = [NSMutableArray array]; [_parentView.points enumerateObjectsUsingBlock:^(WYLineChartPoint * point, NSUInteger idx, BOOL * _Nonnull stop) { shape = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 2, 2)]; shape.center = CGPointMake(point.x, 2*middleY-point.y/* - _parentView.lineBottomMargin*/); shape.backgroundColor = [UIColor clearColor]; [self addSubview:shape]; [controlPoints addObject:shape]; }]; _animationControlPoints = controlPoints; _displayLink.paused = false; self.userInteractionEnabled = false; [UIView animateWithDuration:_animationDuration + 0.5 delay:0.0 usingSpringWithDamping:0.15 initialSpringVelocity:1.40 options:0//UIViewAnimationOptionCurveEaseOut animations:^{ [_animationControlPoints enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL * _Nonnull stop) { view.center = CGPointMake(view.center.x, ((WYLineChartPoint *)_parentView.points[idx]).y); }]; } completion:^(BOOL finished) { if (finished) { _displayLink.paused = true; self.userInteractionEnabled = true; } }];
(1) 首先開啟一個 CADisplayLink定時器,
(2) 根據曲線圖上的點,初始化幾個 子 View,X坐標跟曲線上點的X坐標一樣,Y坐標的值 middleY-point.y+middleY 就是保證 初始化Y坐標是終坐標的關于中線的對稱點。
(3) 開始彈性動畫,設置子視圖的終點,X坐標跟曲線上點的X坐標一樣,Y坐標的值跟曲線上點的Y坐標一樣。 ,在 completion 中對 CADisplayLink定時器暫停。
(4) 在彈性動畫的執行期間,定時器會不斷的獲取某一時刻的所有的子視圖的 坐標 ,并修改 曲線上的點的位置的坐標,并根據 currentLinePathForWave 這個方法繪制出 漸變圖層的 mask的上沿的邊界,然后繪制好整個完整的漸變圖層的 mask的完成path并賦值。
(5) 由于定時器CADisplayLink 的執行速度很快,就達到了如圖的效果。
定時器的代碼如下:
- (void)handleWaveFrameUpdate {
[_parentView.points enumerateObjectsUsingBlock:^(WYLineChartPoint *point, NSUInteger idx, BOOL * _Nonnull stop) {
UIView *view = _animationControlPoints[idx];
point.x = [view wy_centerForPresentationLayer:true].x;
point.y = [view wy_centerForPresentationLayer:true].y;
}];
UIBezierPath *linePath = [self currentLinePathForWave];
_lineShapeLayer.path = linePath.CGPath;
if (_parentView.drawGradient) {
WYLineChartPoint *firstPoint, *lastPoint;
firstPoint = [_parentView.points firstObject];
lastPoint = [_parentView.points lastObject];
UIBezierPath *gradientPath = [UIBezierPath bezierPathWithCGPath:linePath.CGPath];
[gradientPath addLineToPoint:CGPointMake(lastPoint.x, self.wy_boundsHeight)];
[gradientPath addLineToPoint:CGPointMake(firstPoint.x, self.wy_boundsHeight)];
[gradientPath addLineToPoint:firstPoint.point];
_gradientMaskLayer.path = gradientPath.CGPath;
}
}
- (CGPoint)wy_centerForPresentationLayer:(BOOL)isPresentationLayer {
//presentationLayer 是Layer的顯示層(呈現層),需要動畫提交之后才會有值。
if (isPresentationLayer) {
return ((CALayer *)self.layer.presentationLayer).position;
}
return self.center;
}
帶標注的餅狀圖
繪制關鍵步驟:
使用for循環在 drawRect方法中繪制每一個扇形(上篇文章已將講過),因為環外的標注,所以圓環需要小些,否則外環線上的文字繪制起來有可能空間不夠。
根據每一個扇形的中心點位置,通過三角函數計算(三角函數中的參數是弧度,2π即為一個圓周 , iOS中為 M_PI*2,水平右側為0)可以得到圓環外面的小圓的中心點。
通過數值的比例換算,得到每一個扇形的開始弧度和結束弧度值(0~M_PI*2).
得到每一個環外小圓的中心點坐標后,根據該點的X坐標值跟當前頁面中心點的X坐標進行比較,確定小圓尾部的線的朝向以及字體的對其方向(在左側字體向左對齊,在右邊字體向右對齊)
-
環外圓點和直線使用CoreGraphics繪制,文字使用drawInRect: withAttributes繪制,字體左右對齊使用到以下方法:
NSMutableParagraphStyle * paragraph = [[NSMutableParagraphStyle alloc]init]; paragraph.alignment = NSTextAlignmentRight; if (lineEndPointX < [UIScreen mainScreen].bounds.size.width /2.0) { paragraph.alignment = NSTextAlignmentLeft; } drawInRect: withAttributes:@{NSParagraphStyleAttributeName:paragraph}
核心代碼
-(void)drawArcWithCGContextRef:(CGContextRef)ctx andWithPoint:(CGPoint) point andWithAngle_start:(float)angle_start andWithAngle_end:(float)angle_end andWithColor:(UIColor *)color andInt:(int)n { CGContextMoveToPoint(ctx, point.x, point.y); CGContextSetFillColor(ctx, CGColorGetComponents( color.CGColor)); CGContextAddArc(ctx, point.x, point.y, _circleRadius, angle_start, angle_end, 0); CGContextFillPath(ctx); 弧度的中心角度 CGFloat h = (angle_end + angle_start) / 2.0; 使用三角函數計算,環外小圓的中心點坐標 CGFloat xx = self.frame.size.width / 2 + (_circleRadius + 10) * cos(h); CGFloat yy = self.frame.size.height / 2 + (_circleRadius + 10) * sin(h); 畫環外的圓和直線 [self addLineAndnumber:color andCGContextRef:ctx andX:xx andY:yy andInt:n angele:h]; }
股票K線圖
本文參考
首選需要看一下K線圖解, 了解一下一個K線點所需要的數據:
了解一下一個K線點所需要的數據:
陽線代表股票上漲(收盤價大于開盤價), 陰線則代表股票下跌(收盤價小于開盤價), 由此可以看出畫一個K線點需要四個數據, 分別是: 開盤價 - 收盤價 - 最高價 - 最低價, 根據這四個數據畫出上影線實體以及下影線, 柱狀圖(成交量)先不考慮, K線圖畫出來之后, 成交量柱狀圖就不在話下了;
下邊是實現代碼:
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGPoint p1 = CGPointMake(100, 30);
CGPoint p2 = CGPointMake(100, 70);
CGPoint p3 = CGPointMake(100, 120);
CGPoint p4 = CGPointMake(100, 170);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextSetLineWidth(context, 2.0);
// p1 -> p2線段
CGContextMoveToPoint(context, p1.x, p1.y);
CGContextAddLineToPoint(context, p2.x, p2.y);
// p3 -> p4線段
CGContextMoveToPoint(context, p3.x, p3.y);
CGContextAddLineToPoint(context, p4.x, p4.y);
CGContextStrokePath(context);
// 中間實體邊框
CGContextStrokeRect(context, CGRectMake(100 - 14 / 2.0, p2.y, 14, p3.y - p2.y));
}
實現代碼:
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGPoint p1 = CGPointMake(185, 30);
CGPoint p2 = CGPointMake(185, 70);
CGPoint p3 = CGPointMake(185, 120);
CGPoint p4 = CGPointMake(185, 170);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextSetLineWidth(context, 2.0);
// p1 -> p4
CGContextMoveToPoint(context, p1.x, p1.y);
CGContextAddLineToPoint(context, p4.x, p4.y);
CGContextStrokePath(context);
// 畫實心實體
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
CGContextFillRect(context, CGRectMake(p1.x - 14 / 2.0, p2.y, 14, p3.y - p2.y));
}
如果你會畫上面兩種圖,那么K線圖就很簡單了。
將畫K線的代碼封裝成一個方法,然后將最高價最低價開盤價收盤價等轉換成坐標,通過傳入四個參數就可以將K線點畫出來,然后循環調用該方法就好,至于均線就是一個點一個點連接起來的,同樣可以通過線段畫出來,這里就不多說了,還有一個十字線,這個只要會畫線段就會畫十字線,這個也不多說了;
這些掌握了之后就可以繪制專屬自己的K線圖了,其他的都是一些細節小問題,CGContextRef還有很多用法,有興趣的自己可以找度娘,接下來附上我的最終的繪制結果:
關于K線圖可以左右滑動以及放大縮小,而是當手指滑動或者嚙合的時候調用了- (void)drawRect:(CGRect)rect方法,而是又重新畫上去了,因為調用比較頻繁,所以看起來像是在滑動一樣!,所以可以通過手勢來實現捏合的展開合并效果。
/**
* 處理捏合手勢
*
* @param recognizer 捏合手勢識別器對象實例
*/
- (void)handlePinch:(UIPinchGestureRecognizer *)recognizer {
CGFloat scale = recognizer.scale;
recognizer.view.transform = CGAffineTransformScale(recognizer.view.transform, scale, scale); //在已縮放大小基礎下進行累加變化;區別于:使用 CGAffineTransformMakeScale 方法就是在原大小基礎下進行變化
recognizer.scale = 1.0;
}