繪圖-幾個較復雜統計圖案例的實現分析

前言

此本中收錄一些較復雜統計圖案例的實現分析,希望能給需要的朋友帶來靈感。

曲線動態圖

曲線動圖.gif

繪制關鍵步驟:

我們可以看到上圖的動圖是一組組合動畫,共有四部分組成:坐標橫豎虛線的動畫、曲線的動態繪制、小圓點的動畫、漸變區域的動畫。下面逐個分析

  • 坐標橫豎虛線的動畫
    第一步設置一個 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)];

帶彈性的曲線圖

曲線圖彈性動畫.gif

整個效果的實現過程是這樣的:

  • 觸發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線點所需要的數據:

image

陽線代表股票上漲(收盤價大于開盤價), 陰線則代表股票下跌(收盤價小于開盤價), 由此可以看出畫一個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;
 }
股票K線圖github多星開源項目

Y_KLine
chartee

文中動畫曲線圖特效細節參看 WYChart

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

推薦閱讀更多精彩內容