CoreGraphics繪制圖表教程(三)

本篇介紹如何使用CoreGraphics繪制柱狀圖、圓餅圖、環(huán)形圖以及區(qū)域填充

柱狀圖



圓餅圖



環(huán)形圖



區(qū)域填充



有了前兩篇的基礎亭饵,繪制后面這些圖表就簡單的非常了。在第一篇的繪圖三板斧中介紹了所有繪制圖表所需的必要技巧:

  • 繪制折線圖:就是三板斧中的畫一條線换途,圖表中多畫了幾條線段而已

  • 繪制柱狀圖:就是三板斧中繪制一個矩形和填充一個矩形劲厌,圖表中多畫了幾個矩形竖哩,多填充了幾個矩形區(qū)域

  • 繪制圓餅圖:就是三板斧中的畫一個又一個扇形,然后填充脊僚,最后找到準確的位置寫上標題

  • 繪制環(huán)形圖:這個就更簡單了相叁,就是畫幾段圓弧,把圓弧的寬度加大辽幌,就成了圓環(huán)

  • 繪制區(qū)域填充:把畫的線閉合增淹,然后填充,就行了

這幾種圖表過于簡單乌企,所以在這最后一篇集中來寫了虑润,一口氣就都說完吧。

柱狀圖

柱狀圖需要顯示坐標系加酵,所以要繼承自 CoordinateSystem 拳喻。然后要給柱狀圖的類起個名字,就叫 BarChart猪腕,別問我為什么叫這個冗澈,有道詞典說得這么起名。柱狀圖的數據類也要起個名字陋葡,就叫 BarChartData 吧亚亲,這樣有辨識度。

先統(tǒng)一一下思想腐缤, BarChart 代表的是這張柱狀圖捌归, BarChartData 保存的是每一根 “柱體” 的具體信息

ok,兩個類建立好了之后岭粤,要思考一下都有什么屬性或者方法惜索。先看 BarChart 類。

#import "CoordinateSystem.h"

@class BarChartData;
@interface BarChart : CoordinateSystem

@property (nonatomic, strong) NSMutableArray<BarChartData *> *dataArray;
@property (nonatomic, assign) CGFloat barSpacing;
@property (nonatomic, assign) CGFloat barLeftInset;
@property (nonatomic, assign) CGFloat barRightInset;

@end

柱狀圖 BarChart 需要有一個數據集合 NSMutableArray<BarChartData *> *dataArray 剃浇,用來保存將要繪制的 n 個 “柱體” 的具體信息巾兆。然后就是一些常規(guī)的屬性:

  • CGFloat barSpacing 柱體間距

  • CGFloat barLeftInset 最左側柱體與Y軸(坐標系左邊界)的間距

  • CGFloat barRightInset 最右側柱體到坐標系右邊界的距離


看完 BarChart 類,接下來看 BarChartData 類需要來點什么屬性

#import <UIKit/UIKit.h>

@interface BarChartData : NSObject

@property (nonatomic, copy) NSString *valueForX;
@property (nonatomic, copy) NSString *valueForY;
@property (nonatomic, strong) UIColor *borderColor;
@property (nonatomic, strong) UIColor *fillColor;
@property (nonatomic, assign) CGFloat borderWidth;

- (instancetype)initWithValueForX:(NSString *)valueForX valueForY:(NSString *)valueForY;

@end
  • NSString *valueForX X軸刻度

  • NSString *valueForY Y軸刻度

  • UIColor *borderColor 柱體邊框線顏色

  • CGFloat borderWidth 柱體寬度

  • UIColor *fillColor 柱體填充顏色

外加一個初始化方法偿渡,搞定臼寄!



準備工作做得差不多了,下面開始進入正題溜宽。

與折線圖一樣,需要先確定坐標系质帅。Y軸不變适揉,正常繪制緯線和刻度留攒,先計算取值范圍,就是上一篇中的三步走嫉嘀,把最大值和最小值一通修改得到的取值范圍炼邀,用這個取值范圍來確定Y軸的刻度。

X軸需要重寫一下繪制刻度的方法剪侮,用子類方法覆蓋父類方法的方式拭宁,就不用再自己調用一遍了。柱狀圖的效果圖大家也看到了瓣俯,之所以重寫是因為柱狀圖需要分別繪制上下標題(刻度)杰标。柱體上方的標題是它的Y軸刻度,下方的是它的X軸刻度彩匕。


BarChart 類的 drawRect: 方法中:

- (void)drawRect:(CGRect)rect {
    // 計算Y軸取值范圍
    [self calcValueRangeForY];
    [self initAxisY];
    [self initAxisX];
    // 初始化父類 drawRect
    [super drawRect:rect];
    
}

initAxisY 方法和 initAxisX 方法與折線圖中的思路完全一致腔剂,目的還是要確定取值范圍,然后確定Y軸刻度和X軸刻度從而繪制坐標系驼仪。不過這里的坐標系不需要經線掸犬,CoordinateSystem 中經線默認是不顯示的,所以只要不給賦值YES就行了绪爸。

/**
 顯示經線
 */
@property (nonatomic, assign) BOOL displayLongitude;

之后就是父類的 drawRect: 方法開始執(zhí)行湾碎,繪制坐標系。坐標系畫完后會調用預留接口 drawData: 方法奠货,在重寫的 drawData: 方法中可以繪制柱體:

- (void)drawData:(CGRect)rect {
    // 繪制數據
    [self drawBarChart:rect];
}

#pragma mark - 繪制柱體
- (void)drawBarChart:(CGRect)rect {
    CGFloat barWidth = (rect.size.width - self.axisMarginLeft - self.axisMarginRight - self.barLeftInset - self.barRightInset - self.barSpacing * (self.dataArray.count - 1)) / self.dataArray.count;
    
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    if (self.dataArray && self.dataArray.count) {
        CGFloat valueX = self.axisMarginLeft + self.barLeftInset;
        for (BarChartData *barData in self.dataArray) {
            if (barData) {
                CGFloat valueY = (1 - (barData.valueForY.floatValue - self.minValue)/(self.maxValue - self.minValue)) * (rect.size.height - self.axisMarginBottom - self.axisMarginTop) + self.axisMarginTop;
                
                CGRect drawFrame = CGRectMake(valueX, valueY, barWidth, rect.size.height - valueY - self.axisMarginBottom);
                CGContextSetLineWidth(context, barData.borderWidth);
                CGContextSetStrokeColorWithColor(context, barData.borderColor.CGColor);
                CGContextSetFillColorWithColor(context, barData.fillColor.CGColor);
                CGContextFillRect(context, drawFrame);
                CGContextStrokeRect(context, drawFrame);
            }
            //X位移
            valueX = valueX + self.barSpacing + barWidth;
        }
    }
}

BarChart 類中需要重寫父類的繪制標題方法:

#pragma mark - 繪制柱條上下標題
- (void)drawXAxisTitles:(CGRect)rect {
    
    if ([self.longitudeTitles count] <= 0) {
        return;
    }
    
    CGFloat barWidth = (rect.size.width - self.axisMarginLeft - self.axisMarginRight - self.barLeftInset - self.barRightInset - self.barSpacing * (self.dataArray.count - 1)) / self.dataArray.count;
    
    CGFloat barCenterX = self.axisMarginLeft + self.barLeftInset + barWidth/2;
    
    for (int i = 0; i < [self.longitudeTitles count]; i++) {
        // 取出上下標題
        BarChartData *barData = self.dataArray[i];
        NSString *topTitle = [self formatYTitles:barData.valueForY.longLongValue];
        NSString *bottomTitle = (NSString *) [self.longitudeTitles objectAtIndex:i];
        // 統(tǒng)一設置屬性
        UIFont *textFont= self.longitudeFont; //設置字體
        NSMutableParagraphStyle *textStyle=[[NSMutableParagraphStyle alloc]init];//段落樣式
        textStyle.lineBreakMode = NSLineBreakByWordWrapping;
        
        NSDictionary *attrs = @{NSFontAttributeName:textFont,
                                NSParagraphStyleAttributeName:textStyle,
                                NSForegroundColorAttributeName:self.longitudeFontColor};
        // 繪制下標題
        CGSize bottomTitleSize = [bottomTitle boundingRectWithSize:CGSizeMake(100, 100)
                                                 options:NSStringDrawingUsesLineFragmentOrigin
                                                 attributes:attrs
                                                 context:nil].size;
        CGRect bottomTitleRect= CGRectMake(barCenterX - bottomTitleSize.width/2, rect.size.height - self.axisMarginBottom, bottomTitleSize.width, bottomTitleSize.height);
        textStyle.alignment=NSTextAlignmentCenter;
        [bottomTitle drawInRect:bottomTitleRect withAttributes:attrs];
        
        // 繪制上標題
        CGSize topTitleSize = [topTitle boundingRectWithSize:CGSizeMake(100, 100)
                                                    options:NSStringDrawingUsesLineFragmentOrigin
                                                    attributes:attrs
                                                    context:nil].size;
        CGFloat valueY = (1 - (barData.valueForY.floatValue - self.minValue)/(self.maxValue - self.minValue)) * (rect.size.height - self.axisMarginBottom - self.axisMarginTop) + self.axisMarginTop - topTitleSize.height;
        
        CGRect topTitleRect= CGRectMake(barCenterX - topTitleSize.width/2, valueY, topTitleSize.width, topTitleSize.height);
        textStyle.alignment=NSTextAlignmentCenter;
        [topTitle drawInRect:topTitleRect withAttributes:attrs];
        
        // X位移
        barCenterX = barCenterX + self.barSpacing + barWidth;
    }
}

- (NSString *)formatYTitles:(long)value {
    if (value >= 10000) {
        return [NSString stringWithFormat:@"%.2f 萬", value/10000.0f];
    }
    else {
        return [NSString stringWithFormat:@"%ld", value];
    }
}

最后為表示對我大秦帝國歷代君王的追思胜茧,我特意把十字交叉線的標題做了完善,之前折線圖中的十字交叉線的標題都是顯示的百分比仇味,沒有具體完善呻顽。代碼如下:

先在 CoordinateSystem 類中搞一個協(xié)議,為的是讓子類可以自定義十字交叉線的標題丹墨,子類可以返回一個只有兩個元素的數組廊遍,數組的第一個元素是十字交叉線X軸標題,數組的最后一個元素是十字交叉線Y軸標題贩挣。


@protocol CoordinateSystemDelegate <NSObject>

- (NSArray *)crossLineTouchPoint:(CGPoint)touchPoint xPercent:(CGFloat)xPercent yPercent:(CGFloat)yPercent frame:(CGRect)rect;

@end

然后修改一下 CoordinateSystem 類中獲取十字交叉線XY軸刻度的方法喉前,只要子類實現(xiàn)代理方法并返回標題,十字交叉線的XY軸刻度就會按照返回值顯示:

// 獲取十字交叉線的X軸刻度
- (NSString *)calcAxisXGraduate:(CGRect)rect {
    if ([self.delegate respondsToSelector:@selector(crossLineTouchPoint:xPercent:yPercent:frame:)]) {
        NSArray *titles = [self.delegate crossLineTouchPoint:self.singleTouchPoint xPercent:[self touchPointAxisXValue:rect] yPercent:[self touchPointAxisYValue:rect] frame:rect];
        return titles.firstObject;
    }
    return [NSString stringWithFormat:@"%f", [self touchPointAxisXValue:rect]];
}

// 獲取十字交叉線的Y軸刻度
- (NSString *)calcAxisYGraduate:(CGRect)rect {
    if ([self.delegate respondsToSelector:@selector(crossLineTouchPoint:xPercent:yPercent:frame:)]) {
        NSArray *titles = [self.delegate crossLineTouchPoint:self.singleTouchPoint xPercent:[self touchPointAxisXValue:rect] yPercent:[self touchPointAxisYValue:rect] frame:rect];
        return titles.lastObject;
    }
    return [NSString stringWithFormat:@"%f", [self touchPointAxisYValue:rect]];
}



然后回到柱狀圖這里王财,實現(xiàn)代理卵迂,確定十字交叉線的XY軸標題(刻度)。這里進行了柱間區(qū)域的判斷绒净,比如 “嬴稷” 和 “嬴政” 之間的留空區(qū)是不顯示X軸標題的见咒,因為那部分本來就沒有X軸對應的標題,就是一塊空白挂疆。

#pragma mark - 十字交叉線重繪標題
- (NSArray *)crossLineTouchPoint:(CGPoint)touchPoint xPercent:(CGFloat)xPercent yPercent:(CGFloat)yPercent frame:(CGRect)rect {
    NSMutableArray *arrM = [NSMutableArray array];
    // 處理X軸
    CGFloat barWidth = (rect.size.width - self.axisMarginLeft - self.axisMarginRight - self.barLeftInset - self.barRightInset - self.barSpacing * (self.dataArray.count - 1)) / self.dataArray.count;
    CGFloat barLeft = self.axisMarginLeft + self.barLeftInset;
    for (int i = 0; i < self.dataArray.count; i++) {
        CGFloat barRight = barLeft + barWidth;
        if (touchPoint.x >= barLeft && touchPoint.x < barRight) {
            BarChartData *data = self.dataArray[i];
            [arrM addObject:data.valueForX];
            break;
        }
        barLeft = barLeft + barWidth + self.barSpacing;
    }
    
    if (arrM.count == 0) {
        [arrM addObject:@""];
    }
    
    // 處理Y軸
    CGFloat valueRange = self.maxValue - self.minValue;
    CGFloat valueY = valueRange * yPercent;
    NSString *yString = [self formatYTitles:valueY];
    [arrM addObject:yString];
    
    return arrM;
}
柱狀圖最終效果

OK下翎! 柱狀圖大功告成



圓餅圖

繪制圓餅圖不需要顯示坐標系,所以圓餅圖只需繼承 UIView 宝当。給它也取個名字视事,就叫 PieChart 。圓餅圖的數據類也取個名字庆揩,叫 PieChartData俐东,一個非常優(yōu)雅的名字。

看看圓餅圖都需要哪些屬性:

#import <UIKit/UIKit.h>

@class PieChartData;
@interface PieChart : UIView

@property (nonatomic, strong) NSMutableArray<PieChartData *> *dataArray;

/**
 邊框線寬
 */
@property (nonatomic, assign) CGFloat borderWidth;

/**
 邊框線顏色
 */
@property (nonatomic, strong) UIColor *borderColor;

/**
 標題顏色
 */
@property (nonatomic, strong) UIColor *titleColor;

/**
 標題字號
 */
@property (nonatomic, strong) UIFont *titleFont;

@end

再看看圓餅圖數據類需要哪些屬性:

#import <UIKit/UIKit.h>

@interface PieChartData : NSObject

@property (nonatomic, copy) NSString *title;
@property (nonatomic, assign) CGFloat value;
@property (nonatomic, strong) UIColor *fillColor;

- (instancetype)initWithValue:(CGFloat)value fillColor:(UIColor *)fillColor title:(NSString *)title;

@end
  • NSString *title 每個扇形區(qū)域的標題

  • CGFloat value 每個扇形區(qū)域的數值

  • UIColor *fillColor 每個扇形區(qū)域的填充色订晌,這個屬性放在 PieChart 中也可以虏辫,看具體需求而定


回到 PieChart 類中,先初始化各個屬性腾仅,給定默認值:

// 給定圓周率 π 的值
#define PI 3.141592653f

- (id)init {
    self = [super init];
    if (self) {
        //初始化屬性
        [self initProperty];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        //初始化屬性
        [self initProperty];
    }
    return self;
}

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.frame = frame;
        //初始化屬性
        [self initProperty];
    }
    return self;
}

- (void)initProperty {
    self.borderColor = [UIColor whiteColor];
    self.borderWidth = 1;
    self.titleFont = [UIFont systemFontOfSize:14];
    self.titleColor = [UIColor blackColor];
}



畫圓餅圖

- (void)drawRect:(CGRect)rect {
    if (!self.dataArray || !self.dataArray.count) {
        return;
    }
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, self.borderWidth);
    CGContextSetAllowsAntialiasing(context, YES);
    
    // 數據總和
    CGFloat total = [self calcTotalValue];
    // 起始位置弧度
    CGFloat offset = PI * -0.5;
    // 半徑和圓心
    CGFloat radius = MIN(rect.size.width, rect.size.height) / 2;
    CGPoint circleCenter = CGPointMake(rect.size.width/2, rect.size.height/2);
    
    for (PieChartData *data in self.dataArray) {
        if (data) {
            /*
             填充扇形區(qū)域
             */

            // 拿出數據
            CGFloat value = data.value;
            // 計算百分比
            CGFloat percent = value / total;
            // 確定分支弧度
            CGFloat endAngle = percent * 2 * PI;
            // 設定起始點為圓心
            CGContextMoveToPoint(context, circleCenter.x, circleCenter.y);
            // 添加一個圓
            CGContextAddArc(context, circleCenter.x, circleCenter.y, radius, offset, endAngle + offset, 0);
            // 設置填充色
            CGContextSetFillColorWithColor(context, data.fillColor.CGColor);
            // 填充
            CGContextFillPath(context);
            
            /*
             繪制扇形區(qū)域的邊框線
             */
            
            // 設定起始點為圓心
            CGContextMoveToPoint(context, circleCenter.x, circleCenter.y);
            // 添加一個圓弧
            CGContextAddArc(context, circleCenter.x, circleCenter.y, radius, offset, endAngle + offset, 0);
            // 關閉路徑
            CGContextClosePath(context);
            // 設置畫筆顏色
            CGContextSetStrokeColorWithColor(context, self.borderColor.CGColor);
            // 畫線
            CGContextStrokePath(context);
            
            /*
             繪制標題和百分比
             */
            
            // 一半的弧度
            CGFloat halfAngle = offset + endAngle/2;
            // 到圓心的距離
            CGFloat farFromCircleCenter = radius*0.7;
            CGFloat X = circleCenter.x + farFromCircleCenter * cos(halfAngle);
            CGFloat Y = circleCenter.y + farFromCircleCenter * sin(halfAngle);
            
            NSMutableParagraphStyle *textStyle = [[NSMutableParagraphStyle alloc] init];//段落樣式
            textStyle.alignment = NSTextAlignmentCenter;
            textStyle.lineBreakMode = NSLineBreakByWordWrapping;
            NSDictionary *attrs = @{NSFontAttributeName:self.titleFont,
                                    NSParagraphStyleAttributeName:textStyle,
                                    NSForegroundColorAttributeName:self.titleColor};
            // 拼接 標題 和 百分比
            NSString *text = [NSString stringWithFormat:@"%@\n%.2f%%", data.title, percent*100];
            CGSize titleSize = [text boundingRectWithSize:CGSizeMake(100, 100) options:NSStringDrawingUsesLineFragmentOrigin attributes:attrs context:nil].size;
            CGRect textRect= CGRectMake(X - titleSize.width / 2, Y - titleSize.height / 2, titleSize.width, titleSize.height);
            [text drawInRect:textRect withAttributes:attrs];
            
            // 弧度積累偏移
            offset += endAngle;
        }
    }
}

- (CGFloat)calcTotalValue {
    if (self.dataArray && self.dataArray.count) {
        CGFloat sum = 0;
        for (PieChartData *data in self.dataArray) {
            sum += data.value;
        }
        return sum;
    }
    return 0;
}

說一下思路:

原作《Cocoa-Charts》中繪制圓餅圖的代碼中有很多不必要的代碼我全部給刪掉了乒裆。比如填充時不需要設定線寬也不需要設定畫筆顏色,或者填充時不需要閉合路徑推励,因為填充時自動閉合路徑鹤耍,寫了也白寫。

首先验辞,畫圓弧的起始弧度定在了(-π/2)稿黄,請看下圖來感受一下(-π/2)在哪:

計算全部分支的數值總和,然后計算每個分支的數值在總和中的占比跌造,這個比例就是此分支在整個圓中要占的比例杆怕,用這個比例值乘以 2π 來確定它的弧度大小。

先填充扇形再畫扇形邊框線壳贪,這些步驟都很簡單陵珍。重點是標題的位置,要怎么放才會真的居中违施?

原作中的標題位置有點尷尬互纯,并沒有做到真正居中,讓我這個強迫癥患者看著很是難受磕蒲。所以在標題位置這里重新做了一下優(yōu)化留潦。

我畫了一個示意圖,我理想中的標題位置如圖所示:


綠色虛線是這一分支圓弧的一半弧度辣往,即該圓弧的中心弧度線

CGFloat halfAngle = offset + endAngle/2;

紅色實線的長度是到圓心 70%半徑 的距離

CGFloat farFromCircleCenter = radius*0.7;

確定了長度和弧度兔院,就有了我們要的點,即文字的中心點站削,圖中的黑色圓點

兩條藍色實線和紅色實線形成了直角三角形坊萝,就是該中心點用來計算XY坐標的三角函數示意

文字標題的中心點 在該扇形中心弧度線上 距離圓心 70%半徑 的位置

CGFloat X = circleCenter.x + farFromCircleCenter * cos(halfAngle);
CGFloat Y = circleCenter.y + farFromCircleCenter * sin(halfAngle);

?
這樣文字的位置可以根據代碼 radius*0.7 中的比例來設置文字距離圓心的距離。

圓餅圖畫完嘍R傺摺8匾薄街氢!




環(huán)形圖

環(huán)形圖要繼承自圓餅圖扯键,因為兩者基本是一個思路,用的屬性和數據都一樣珊肃。沒什么好說的荣刑,看懂圓餅圖就明白這個是怎么來的,不比比伦乔,直接上代碼

#import "PieChart.h"

@interface AnnulusChart : PieChart

/**
 * 圓環(huán)寬度占半徑的多少比例
 *
 * 取值范圍 0-1
 */
@property (nonatomic, assign) CGFloat annulusWidthPercentToRadius;

@end
#import "AnnulusChart.h"
#import "PieChartData.h"

#define PI 3.141592653f

@implementation AnnulusChart

- (void)initProperty {
    [super initProperty];
    self.annulusWidthPercentToRadius = 0.3;
}

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetAllowsAntialiasing(context, YES);
    CGContextSetStrokeColorWithColor(context, self.borderColor.CGColor);
    
    if (self.dataArray && self.dataArray.count && self.annulusWidthPercentToRadius > 0 && self.annulusWidthPercentToRadius < 1) {
        
        // 數據總和
        CGFloat total = [self calcTotalValue];
        
        // 起始位置弧度
        CGFloat offset = PI * -0.5;
        // 半徑和圓心
        CGFloat radius = MIN(rect.size.width, rect.size.height) / 2;
        CGPoint circleCenter = CGPointMake(rect.size.width/2, rect.size.height/2);
        
        // 遍歷每一條數據列表
        for (int j = 0; j < [self.dataArray count]; j++) {
            PieChartData *entity = [self.dataArray objectAtIndex:j];
            
            //角度
            CGFloat sweep = entity.value * 2 * PI / total;
            
            // 邊線繪制的寬度是圓周內一半外一半
            CGContextSetLineWidth(context, radius * self.annulusWidthPercentToRadius);
            CGContextSetStrokeColorWithColor(context, entity.fillColor.CGColor);
            CGContextAddArc(context, circleCenter.x, circleCenter.y, radius * (1 - self.annulusWidthPercentToRadius / 2), offset, offset + sweep, 0);
            CGContextStrokePath(context);
            
            //調整偏移
            offset = offset + sweep;
            
        }
    }
}

@end

思路還是圓餅圖的思路厉亏,就是這個 “環(huán)” 需要考慮一下。

原作者的做法就是上面代碼中的實現(xiàn)方式烈和,圓環(huán)用很粗的邊框線來實現(xiàn)爱只。這個邊框線的寬度就直接用屬性 annulusWidthPercentToRadius 乘以半徑來定。

邊框線如果很寬你就會看到招刹,邊框線不是全部繪制在圓周之外恬试,而是一半在圓周內,一半在圓周外疯暑。下圖白色實線就是圓周線實際的中心位置




還有一種辦法就是畫一個圓餅圖训柴,和前面的圓餅圖完全一樣,然后在畫一個與底色相同的圓覆蓋上妇拯。這樣的好處就是可以有邊框線幻馁,圓環(huán)的分界線會更明顯。具體怎么實現(xiàn)就看你的需求了越锈。


區(qū)域填充

區(qū)域填充就是在畫折線圖仗嗦,需要填充的部分把它們的路徑閉合進行填充就可以了。

這個填充需要注意兩點

要點一

就是在第一篇文章提到的:CGContextFillPath 填充最多可以兩條線的路徑實現(xiàn)閉合甘凭,三條線的路徑就失靈了稀拐。

什么意思? 請看一條線示意圖:


上圖給出起點对蒲,然后通過三次調用 CGContextAddLineToPoint 畫了三條線段钩蚊,從而連接成一條線的路徑,這三個線段連成的線路徑是 “一條線的路徑”5赴E槁摺!這不是三條線而是一條線7耗瘛r鹋亍!

因為只有一條線,沒有超過兩條線刚操,所以可以進行填充闸翅。此時填充這條路徑的話會自動閉合起始點和終點,填充的是下圖這個區(qū)域:




兩條線的示意圖:

上圖中是兩條線組成的閉合區(qū)域菊霜,也可以進行填充坚冀,填充的就是上圖所示的閉合區(qū)域。

如果是下圖這樣的情況鉴逞,也會自動閉合:



要點二

兩個不相干的封閉路徑都可以被一次性正常填充记某,比如多個矩形


上圖這兩個路徑可以一次性進行填充



說了這么多的前提,那到底我在文章最前面給出的區(qū)域填充效果圖是怎么填充的呢构捡?




原作者并不是如我所想液南,使用要點一在兩條線之間的區(qū)域一次性填充。而是畫了無數個閉合的矩形勾徽,使用要點二進行一次性多個填充滑凉。把代碼修改一下就可以再現(xiàn)原作者的思路,我們不填充而是進行畫線:
image.png

這就是原作者的思路喘帚。

.h文件

#import "LineChart.h"

@interface AreaChart : LineChart

@end

.m 文件

#import "AreaChart.h"
#import "LineData.h"
#import "LinePointData.h"

@implementation AreaChart



- (void)drawData:(CGRect)rect {
    
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, 1);
    CGContextSetAllowsAntialiasing(context, YES);
    
    if (self.linesArray != NULL) {
        // 經線間距
        CGFloat longitudeSpacing = 0;
        // 坐標
        __block CGFloat valueX = 0;
        __block CGFloat lastX = 0;
        __block CGFloat lastY = 0;
        // 逐條繪制
        for (LineData *line in self.linesArray) {
            if (!line || !line.linePointsDataArray || line.linePointsDataArray.count < 2) continue;
            // 配置線條
            CGContextSetStrokeColorWithColor(context, line.lineColor.CGColor);
            CGContextSetLineWidth(context, line.lineWidth);

            longitudeSpacing = (rect.size.width - self.axisMarginLeft - self.axisMarginRight) / (line.linePointsDataArray.count - 1);

            valueX = self.axisMarginLeft;

            [line.linePointsDataArray enumerateObjectsUsingBlock:^(LinePointData * _Nonnull point, NSUInteger idx, BOOL * _Nonnull stop) {
                // 計算點的Y坐標
                CGFloat valueY = (1 - (point.valueForY.floatValue - self.minValue) / (self.maxValue - self.minValue)) * (rect.size.height - self.axisMarginTop - self.axisMarginBottom) + self.axisMarginTop;
                // 第一個初始點不畫線
                if (idx == 0) {
                    CGContextMoveToPoint(context, valueX, valueY);
                    lastY = valueY;
                }
                else {
                    CGContextAddLineToPoint(context, valueX, valueY);
                    lastY = valueY;
                }
                // X坐標移動
                valueX = valueX + longitudeSpacing;
            }];
            // 繪制路徑
            CGContextStrokePath(context);
        }
        
        LineData *line1 = [self.linesArray objectAtIndex:0];
        LineData *line2 = [self.linesArray objectAtIndex:1];


        if (line1 != NULL && line2 != NULL) {
            //設置線條顏色
            CGContextSetStrokeColorWithColor(context, [UIColor greenColor].CGColor);
            //獲取線條數據
            NSArray *line1Points = line1.linePointsDataArray;
            NSArray *line2Points = line2.linePointsDataArray;

            // 點線距離
            CGFloat lineLength = ((rect.size.width - self.axisMarginLeft - self.axisMarginRight) / ([line1.linePointsDataArray count] - 1));
            //起始點
            valueX = super.axisMarginLeft;
            //遍歷并繪制線條
            for (int j = 0; j < [line1Points count]; j++) {
                LinePointData *line1Point = [line1Points objectAtIndex:j];
                LinePointData *line2Point = [line2Points objectAtIndex:j];

                //獲取終點Y坐標
                CGFloat valueY1 = (1 - (line1Point.valueForY.floatValue - self.minValue) / (self.maxValue - self.minValue)) * (rect.size.height - self.axisMarginTop - self.axisMarginBottom) + self.axisMarginTop;
                CGFloat valueY2 = (1 - (line2Point.valueForY.floatValue - self.minValue) / (self.maxValue - self.minValue)) * (rect.size.height - self.axisMarginTop - self.axisMarginBottom) + self.axisMarginTop;

                //繪制線條路徑
                if (j == 0) {
                    CGContextMoveToPoint(context, valueX, valueY1);
                    CGContextAddLineToPoint(context, valueX, valueY2);
                    CGContextMoveToPoint(context, valueX, valueY1);
                } else {
                    CGContextAddLineToPoint(context, valueX, valueY1);
                    CGContextAddLineToPoint(context, valueX, valueY2);
                    CGContextAddLineToPoint(context, lastX, lastY);

                    CGContextMoveToPoint(context, valueX, valueY1);
                }
                lastX = valueX;
                lastY = valueY2;
                //X位移
                valueX = valueX + lineLength;
            }
            CGContextClosePath(context);
            CGContextSetAlpha(context, 0.5);
            CGContextSetFillColorWithColor(context, [UIColor yellowColor].CGColor);
            CGContextFillPath(context);
//            CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
//            CGContextStrokePath(context);
        }
        
    }
}

- (void)initAxisX {
    [super initAxisX];
}

- (void)initAxisY {
    [super initAxisY];
}

@end



我們還可以使用 要點一 來填充畅姊,左右兩條豎線和第一條線連接在一起,第二條線和第一條線封閉啥辨。


Github示例源碼

鏈接地址:CoreGraphicsDrawChart

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末柔逼,一起剝皮案震驚了整個濱河市纬霞,隨后出現(xiàn)的幾起案子弓摘,更是在濱河造成了極大的恐慌娜膘,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件级乍,死亡現(xiàn)場離奇詭異舌劳,居然都是意外死亡,警方通過查閱死者的電腦和手機玫荣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進店門甚淡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人捅厂,你說我怎么就攤上這事贯卦。” “怎么了焙贷?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵撵割,是天一觀的道長。 經常有香客問我辙芍,道長啡彬,這世上最難降的妖魔是什么羹与? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮庶灿,結果婚禮上纵搁,老公的妹妹穿的比我還像新娘。我一直安慰自己往踢,他們只是感情好腾誉,可當我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著菲语,像睡著了一般妄辩。 火紅的嫁衣襯著肌膚如雪惑灵。 梳的紋絲不亂的頭發(fā)上山上,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天,我揣著相機與錄音英支,去河邊找鬼佩憾。 笑死,一個胖子當著我的面吹牛干花,可吹牛的內容都是我干的妄帘。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼池凄,長吁一口氣:“原來是場噩夢啊……” “哼抡驼!你這毒婦竟也來了?” 一聲冷哼從身側響起肿仑,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤致盟,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后尤慰,有當地人在樹林里發(fā)現(xiàn)了一具尸體馏锡,經...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年伟端,在試婚紗的時候發(fā)現(xiàn)自己被綠了杯道。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡责蝠,死狀恐怖党巾,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情霜医,我是刑警寧澤齿拂,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站支子,受9級特大地震影響创肥,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一叹侄、第九天 我趴在偏房一處隱蔽的房頂上張望巩搏。 院中可真熱鬧,春花似錦趾代、人聲如沸贯底。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽禽捆。三九已至,卻和暖如春飘哨,著一層夾襖步出監(jiān)牢的瞬間胚想,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工芽隆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留浊服,地道東北人。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓胚吁,卻偏偏與公主長得像牙躺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子腕扶,可洞房花燭夜當晚...
    茶點故事閱讀 45,585評論 2 359

推薦閱讀更多精彩內容