需求簡(jiǎn)介
類似支付寶那樣的錢(qián)包工具,有很大一個(gè)類別理財(cái),經(jīng)常出現(xiàn)收益率曲線之類的,這個(gè)就是折線圖,大概的樣子如下

如何實(shí)現(xiàn)
采用貝塞爾曲線自己畫(huà),采用
CAShapeLayer進(jìn)行組合,也是可以做的。只是有點(diǎn)復(fù)雜,操心的內(nèi)容有點(diǎn)多。采用第三方庫(kù),這也是一個(gè)好主意。有現(xiàn)實(shí)需求,自己實(shí)現(xiàn)又煩,好用的第三方庫(kù)就來(lái)了。經(jīng)過(guò)比較,我們可以選擇 Charts
這個(gè)第三方庫(kù)是用
swift寫(xiě)的,在目前的object-c工程中使用有點(diǎn)麻煩。不過(guò),為了和Android保持一致,這點(diǎn)麻煩算不了什么。這些糾結(jié)的過(guò)程,已經(jīng)在另一篇文章里寫(xiě)了:關(guān)于圖表庫(kù)的選擇
包裝Charts
Charts中代表折線圖的是LineChartView,從BarLineChartViewBase,ChartViewBase,NSUIView,UIView一路繼承過(guò)來(lái)。
- 創(chuàng)建一個(gè)內(nèi)部屬性成員
LineChartView,作為我們的自定義view的子view,并且占滿所有空間。我們的自定義view只是一個(gè)容器。
@interface KJTLineChartView ()
@property (strong, nonatomic) LineChartView *lineView;
@end
- 創(chuàng)建子視圖
#pragma mark - private
- (void)setup {
self.lineView = [[LineChartView alloc] init];
[self addSubview:self.lineView];
}
- 自定義視圖只是作為容器,將剛剛創(chuàng)建的折線圖鋪滿整個(gè)空間。
// 布局
- (void)layoutSubviews {
[self.lineView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(0);
make.bottom.mas_equalTo(0);
make.left.mas_equalTo(0);
make.right.mas_equalTo(0);
}];
}
- 由于沒(méi)有數(shù)據(jù),所以現(xiàn)在是空白:

- 圖中的文案,在
ChartViewBase中可以找到:

設(shè)置數(shù)據(jù)
圖中數(shù)據(jù)點(diǎn)的包含
(x,y)兩個(gè)維度的信息,數(shù)據(jù)類型都是double。在大多數(shù)的介紹文章中,都是把x軸的信息簡(jiǎn)化為序號(hào)index,這當(dāng)然是可以的。這次的需求是一張“七日年化收益率”折線圖,'y'軸是收益率,數(shù)據(jù)類型是
double,沒(méi)問(wèn)題。橫軸是七天的日期,是“月月-日日”的格式。考慮到NSDate類型轉(zhuǎn)化為double和字符串都很方便,所以x軸數(shù)據(jù)可以直接是“時(shí)間戳”,也就是從1970年到現(xiàn)在所過(guò)的秒數(shù)。所以這次的做法是將NSDate類型轉(zhuǎn)化為double,直接作為x軸數(shù)據(jù)來(lái)實(shí)現(xiàn)的。關(guān)于數(shù)據(jù),設(shè)置過(guò)程主要有以下幾步
- 需要封裝成
一個(gè)ChartDataEntry結(jié)構(gòu); - 多個(gè)
ChartDataEntry然后形成一個(gè)數(shù)組之后; - 將數(shù)組再包裝成一個(gè)
LineChartDataSet結(jié)構(gòu),這個(gè)結(jié)構(gòu)代表一條曲線; - 將多個(gè)(包括1個(gè))
LineChartDataSet結(jié)構(gòu)包裝成一個(gè)數(shù)組,所以可以同時(shí)顯示多條曲線; - 將這個(gè)數(shù)組包裝成
LineChartData結(jié)構(gòu); - 用得到的這個(gè)結(jié)構(gòu),賦值給
LineChartView的data成員;
- 設(shè)計(jì)了一個(gè)對(duì)外接口,提供標(biāo)簽數(shù)組和y值數(shù)組
// x軸是日期,y軸是數(shù)據(jù)
- (void)updateWithDates:(NSArray<NSDate *> *)dates values:(NSArray<NSNumber *> *)values {
//
// 日期和數(shù)據(jù)組成一個(gè)點(diǎn),形成一條折線
//
// 1. 將日期和數(shù)據(jù)取出,包裝成ChartDataEntry,組成新的數(shù)組
// x軸是日期, y軸是數(shù)據(jù)
NSMutableArray<ChartDataEntry *> *entrys = [NSMutableArray array];
NSInteger count = dates.count;
for (NSInteger index = 0; index < count; index++) {
double x = (double)dates[index].timeIntervalSince1970;
double y = [values[index] doubleValue];
ChartDataEntry *entry = [[ChartDataEntry alloc] initWithX:x y:y];
[entrys addObject:entry];
}
// 2. 組裝LineChartDataSet結(jié)構(gòu),并做相應(yīng)的配置
LineChartDataSet *dataSet = [[LineChartDataSet alloc] initWithValues:entrys label:nil];
// 配置先不做,看默認(rèn)效果
// 3. 組裝LineChartDataSet數(shù)組,這里只需要一個(gè)dataSet
NSArray *dataSets = [NSArray arrayWithObject:dataSet];
// 4. 組裝LineChartData結(jié)構(gòu)
LineChartData *data = [[LineChartData alloc] initWithDataSets:dataSets];
// 5. 賦值
self.lineView.data = data;
}
- 在調(diào)用的ViewController中,給幾個(gè)測(cè)試數(shù)據(jù)
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSDate *today = [NSDate date];
NSArray *dates = @[today, [today kjtDateByAddingDays:1], [today kjtDateByAddingDays:2], [today kjtDateByAddingDays:3], [today kjtDateByAddingDays:4], [today kjtDateByAddingDays:5], [today kjtDateByAddingDays:6]];
NSArray *values = @[@3.729, @3.683, @2.8, @3.1, @4.123, @3.587, @3.7482];
[self.lineChartView updateWithDates:dates values:values];
}
運(yùn)行起來(lái)之后,效果是這樣的:

整體設(shè)置
默認(rèn)的可以拖動(dòng),可以放大,雙擊也可以放大,y軸有兩個(gè)。在這里,這些都不需要,可以做如下配置:
//
// 設(shè)置chart view 整體
//
// 選中處理等
self.lineView.delegate = self;
// 隱藏說(shuō)明圖標(biāo)
self.lineView.legend.form = ChartLegendFormNone;
// 無(wú)數(shù)據(jù)提示信息
self.lineView.noDataText = @"暫無(wú)數(shù)據(jù)";
// 取消Y軸縮放
self.lineView.scaleYEnabled = NO;
// 取消X軸縮放
self.lineView.scaleXEnabled = NO;
// 取消雙擊縮放
self.lineView.doubleTapToZoomEnabled = NO;
// 取消拖拽圖標(biāo)
self.lineView.dragEnabled = NO;
// 加載動(dòng)畫(huà)
[self.lineView animateWithXAxisDuration:1.0];
// 隱藏右Y軸
self.lineView.rightAxis.enabled = NO;
加上這些配置之后,右y軸沒(méi)了,也不能放大了,legend也沒(méi)有了,并且還有生成動(dòng)畫(huà)。大致效果如下:

配置左Y軸
根據(jù)設(shè)計(jì)UI圖,配置左Y軸的相關(guān)屬性
//
// 設(shè)置左Y軸
//
// y軸展示多少個(gè)
self.lineView.leftAxis.labelCount = 6;
// 設(shè)置Y軸的最小值
self.lineView.leftAxis.axisMinimum = 0;
// Y軸顏色
self.lineView.leftAxis.axisLineColor = kColorWithHexA(0x000000, 0.24);
// 文字顏色
self.lineView.leftAxis.labelTextColor = kColorWithHexA(0x000000, 0.24);
// 文字字體
self.lineView.leftAxis.labelFont = kFontSystem10;
// 網(wǎng)格線顏色
self.lineView.leftAxis.gridColor = kColorWithHex(0xECECF1);
這時(shí)的效果如下:

配置X軸
//
// 設(shè)置X軸
//
// 設(shè)置x軸數(shù)據(jù)在底部
self.lineView.xAxis.labelPosition = XAxisLabelPositionBottom;
// x軸不畫(huà)網(wǎng)格線
self.lineView.xAxis.drawGridLinesEnabled = NO;
// X軸顏色
self.lineView.xAxis.axisLineColor = kColorWithHexA(0x000000, 0.24);
// 文字顏色
self.lineView.xAxis.labelTextColor = kColorWithHexA(0x000000, 0.24);
// 文字字體
self.lineView.xAxis.labelFont = kFontSystem10;

配置折線
// 是否在拐點(diǎn)處顯示數(shù)據(jù)
dataSet.drawValuesEnabled = NO;
// 折線寬度
dataSet.lineWidth = 2;
// 折線顏色
[dataSet setColor:kRedColor];
// 是否繪制拐點(diǎn)
dataSet.drawCirclesEnabled = NO;
// 是否填充顏色
dataSet.drawFilledEnabled = YES;
// 填充顏色
dataSet.fillColor = kRedColor;
// 填充顏色透明度
dataSet.fillAlpha = 0.20;
// 去掉左下角“DataSet”字符串
dataSet.label = nil;

格式化坐標(biāo)軸標(biāo)簽
標(biāo)簽格式的設(shè)置比較特殊,需要一個(gè)單獨(dú)的類,實(shí)現(xiàn)IChartAxisValueFormatter協(xié)議。只有一個(gè)函數(shù)

-
Y軸格式化,將double類型的數(shù)據(jù),轉(zhuǎn)化為3位小數(shù)的字符串。并且為了避免擁擠,0不顯示
@implementation KJTLabelFormatterY
#pragma mark - IChartAxisValueFormatter
- (NSString *)stringForValue:(double)value axis:(ChartAxisBase *)axis {
// 第一個(gè)Y軸坐標(biāo)不顯示,避免和第一個(gè)X坐標(biāo)擁擠
if (value == 0) {
return @"";
}
return [NSString stringWithFormat:@"%0.3f", value];
}
@end
配置的地方,指定這個(gè)自定義的類
// 格式化y標(biāo)簽
self.lineView.leftAxis.valueFormatter = [[KJTLabelFormatterY alloc] init];

- X軸格式化,這里就很簡(jiǎn)單,將時(shí)間戳轉(zhuǎn)換為“月月-日日”格式的日期字符串就可以了。
@implementation KJTLabelFormatterX
#pragma mark - IChartAxisValueFormatter
- (NSString *)stringForValue:(double)value axis:(ChartAxisBase *)axis {
NSDate *date = [NSDate dateWithTimeIntervalSince1970:(NSTimeInterval)value];
return [date kjtStringWithFormat:@"MM-dd"];
}
@end
配置的地方:
// 格式化x軸標(biāo)簽
self.lineView.xAxis.valueFormatter = [[KJTLabelFormatterX alloc] init];

設(shè)置選中標(biāo)簽
提供了一個(gè)ChartMarkerView視圖,作為選中之后彈出的“氣泡視圖”。這個(gè)可以看做一個(gè)容器,具體的樣子,完全可以自定義。這里,就用了一個(gè)UILabel和一個(gè)UIImageView來(lái)實(shí)現(xiàn)。
很多文章中提到的BalloonMarker并不存在,這是官方demo中自己寫(xiě)的一個(gè)自定義視圖,并且是swift的,不能直接用。
“氣泡視圖”ChartMarkerView的原點(diǎn),默認(rèn)在選中數(shù)據(jù)點(diǎn)上,效果會(huì)出現(xiàn)在下方。如果要做到UI上那樣在選中點(diǎn)上方,需要通過(guò)offset屬性來(lái)調(diào)節(jié)位置。這個(gè)地方相對(duì)布局不方便,直接用frame比較好。這個(gè)過(guò)程相對(duì)比較繁瑣,需要耐心調(diào)整。
//
// 設(shè)置選中時(shí)候的"氣泡視圖"
//
// 這個(gè)標(biāo)記視圖將以數(shù)據(jù)點(diǎn)為起點(diǎn)布局
ChartMarkerView *markerView = [[ChartMarkerView alloc] init];
// 顯示y軸數(shù)據(jù)的標(biāo)簽
self.markerLabel = [[UILabel alloc] init];
self.markerLabel.layer.cornerRadius = 9;
self.markerLabel.layer.masksToBounds = YES;
self.markerLabel.font = kFontSystem12;
self.markerLabel.textAlignment = NSTextAlignmentCenter;
self.markerLabel.textColor = [UIColor whiteColor];
self.markerLabel.backgroundColor = kRedColor;
[markerView addSubview:self.markerLabel];
// 選中小圖標(biāo)
UIImageView *imageView = [[UIImageView alloc] init];
imageView.image = [UIImage imageNamed:@"折線圖選中"];
[markerView addSubview:imageView];
// 布局,標(biāo)簽和圖標(biāo)右對(duì)齊,間隔為4
self.markerLabel.frame = CGRectMake(0, 0, 49, 18);
imageView.frame = CGRectMake(49-14, 18+4, 14, 14);
// 設(shè)置偏移量,使數(shù)據(jù)點(diǎn)和小圖標(biāo)的中心點(diǎn)重合
CGFloat x = imageView.center.x;
CGFloat y = imageView.center.y;
markerView.offset = CGPointMake(-x, -(y+7));
// 設(shè)置折線圖的標(biāo)簽視圖
markerView.chartView = self.lineView;
self.lineView.marker = markerView;
UILabel上要顯示y的值,需要實(shí)現(xiàn)ChartViewDelegate協(xié)議
#pragma mark - ChartViewDelegate
- (void)chartValueSelected:(ChartViewBase *)chartView entry:(ChartDataEntry *)entry highlight:(ChartHighlight *)highlight {
self.markerLabel.text = [NSString stringWithFormat:@"%0.4f", entry.y];
}
效果圖如下,和UI設(shè)計(jì)圖比較接近了。

- 在頂部,如果空間不夠,由于受到邊界影響,氣泡視圖會(huì)出現(xiàn)移位現(xiàn)象,導(dǎo)致圖片對(duì)不準(zhǔn)。

- 第
1個(gè)點(diǎn),由于有坐標(biāo)軸的影響,位置也會(huì)偏移一些

-
UI圖上是"山頂",效果相對(duì)較好,如果位置出現(xiàn)在"山谷",效果就差一點(diǎn)了

小結(jié)
相對(duì)于自己來(lái)寫(xiě),省力很多,整體效果也好很多。雖然配置比較麻煩,思路有點(diǎn)奇特。推薦使用。