iOS 自定義折線圖
效果及 Demo
由于產(chǎn)品需求要展示分?jǐn)?shù)麻捻,并且60分以下展示需要壓縮比例,而開源的圖表庫太過復(fù)雜并且有些無法滿足需求呀袱,學(xué)習(xí)成本偏高贸毕,于是決定自己寫一個(gè),效果如下圖夜赵。
視圖層級及設(shè)計(jì)思路
- 使用
UICollectionView
實(shí)現(xiàn)滾動效果明棍,并且部分展示元素可以復(fù)用,如下圖中日期label
寇僧、虛線及圓點(diǎn) - 使用
CAShapeLayer
+UIBezierPath
實(shí)現(xiàn)折線及陰影繪制
代碼實(shí)現(xiàn)
圖表實(shí)現(xiàn)代碼
#import "ZHLineChartView.h"
#import "ZHLineChartConst.h"
#import "ZHLineChartModel.h"
@interface ZHLineChartView ()
@end
@implementation ZHLineChartView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// 這步是因?yàn)樘福衅渌?layer 時(shí),折線圖需要展示在最底層
self.layer.zPosition = -1.f;
}
return self;
}
#pragma mark - Load Subviews
- (void)makeDraw {
// x軸的 y
CGFloat moveStartY = kChartViewH;
UIBezierPath *aPath = [UIBezierPath bezierPath];//填充鍍層
UIBezierPath *aPathLine = [UIBezierPath bezierPath];//線
// 所有經(jīng)過的坐標(biāo)點(diǎn)婉宰,畫線
// NSInteger startI = 0; // 用于計(jì)算無效點(diǎn)陰影
BOOL ignoreZeroPoint = YES;
for (int i = 0; i < _dataModels.count; i++) {
CGFloat yPoint = _dataModels[i].yPoint;
if (ignoreZeroPoint && yPoint == kInvaildYPoint) { //忽略前面為0的點(diǎn)(產(chǎn)品需求)
continue;
}
CGFloat xPoint = kLineChartMoveStartX + i * kLineChartMoveW;
if (ignoreZeroPoint) {
//設(shè)置開始的坐標(biāo)點(diǎn)
//取得第一個(gè)有效點(diǎn)的 index
// startI = i;
[aPath moveToPoint:CGPointMake(xPoint, moveStartY)]; //鍍層要從 x 軸開始畫歌豺,再添加第一個(gè)點(diǎn)坐標(biāo)
[aPath addLineToPoint:CGPointMake(xPoint, yPoint)];
[aPathLine moveToPoint:CGPointMake(xPoint, yPoint)];
ignoreZeroPoint = NO; //開始畫點(diǎn),不再忽略為0的點(diǎn)
} else {
[aPath addLineToPoint:CGPointMake(xPoint, yPoint)];
[aPathLine addLineToPoint:CGPointMake(xPoint, yPoint)];
}
}
//鍍層收邊
CGFloat lineW = kLineChartMoveStartX + (_dataModels.count - 1) * kLineChartMoveW;
[aPath addLineToPoint:CGPointMake(lineW, moveStartY)];
[aPath closePath];
// 前面為 0 的點(diǎn)灰色蒙層心包,UI 后來又不要了
// CAGradientLayer *grayLayer = [CAGradientLayer layer];
// grayLayer.colors = @[(__bridge id)[[UIColor lightGrayColor] colorWithAlphaComponent:0.f].CGColor, (__bridge id)[[UIColor lightGrayColor] colorWithAlphaComponent:0.4f].CGColor];
// grayLayer.frame = CGRectMake(kLineChartMoveStartX, 0.f, startI * kLineChartMoveW, moveStartY);
// grayLayer.startPoint = CGPointZero;
// grayLayer.endPoint = CGPointMake(1.f, 1.f);
// [self.layer addSublayer:grayLayer];
[self addGradientLayerWithPath:aPath];
[self addLineLayerWithPath:aPathLine];
[self addXLineLayer];
}
- (void)addXLineLayer {
CGFloat lineW = kLineChartMoveStartX + (_dataModels.count - 1) * kLineChartMoveW;
//x 軸
UIBezierPath *xLinePath = [UIBezierPath bezierPath];
[xLinePath moveToPoint:CGPointMake(kLineChartMoveStartX - kLineChartMoveW / 2.f, kChartViewH)];
[xLinePath addLineToPoint:CGPointMake(kLineChartMoveStartX + lineW, kChartViewH)];
CAShapeLayer *xLineLayer = [CAShapeLayer layer];
xLineLayer.strokeColor = [UIColor lightGrayColor].CGColor;
xLineLayer.lineWidth = 0.5f;
xLineLayer.path = xLinePath.CGPath;
xLineLayer.zPosition = -2;
[self.layer addSublayer:xLineLayer];
}
- (void)addLineLayerWithPath:(UIBezierPath *)path {
//折線
CAShapeLayer *shapelayerLine = [CAShapeLayer layer];
//設(shè)置邊框顏色类咧,就是上邊畫的,線的顏色
shapelayerLine.strokeColor = [[UIColor orangeColor] CGColor];
//設(shè)置填充顏色 如果不需要[UIColor clearColor]
shapelayerLine.fillColor = [[UIColor clearColor] CGColor];
//就是這句話在關(guān)聯(lián)彼此(UIBezierPath和CAShapeLayer):
shapelayerLine.path = path.CGPath;
[self.layer addSublayer:shapelayerLine];
}
- (void)addGradientLayerWithPath:(UIBezierPath *)path {
//獲取總共的長度
CGFloat lineW = kLineChartMoveStartX + (_dataModels.count - 1) * kLineChartMoveW;
CAShapeLayer *shapelayer = [CAShapeLayer layer];
//設(shè)置邊框顏色蟹腾,就是上邊畫的痕惋,線的顏色
shapelayer.strokeColor = [[UIColor orangeColor] CGColor];
//設(shè)置填充顏色
shapelayer.fillColor = [[UIColor orangeColor] CGColor];
//就是這句話在關(guān)聯(lián)彼此(UIBezierPath和CAShapeLayer):
shapelayer.path = path.CGPath;
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.colors = @[(__bridge id)[[UIColor orangeColor] colorWithAlphaComponent:0.4f].CGColor, (__bridge id)[[UIColor orangeColor] colorWithAlphaComponent:0.f].CGColor];
// gradientLayer.locations = @[@0.f, @0.5f];
gradientLayer.startPoint = CGPointMake(0.5, 0.f);
gradientLayer.endPoint = CGPointMake(0.5, 1.f);
gradientLayer.shadowPath = path.CGPath;
gradientLayer.frame = CGRectMake(0, 0, lineW, kChartViewH);
gradientLayer.mask = shapelayer;
[self.layer addSublayer:gradientLayer];
}
#pragma mark - Setter
- (void)setDataModels:(NSArray<ZHLineChartModel *> *)dataModels {
_dataModels = dataModels;
[self makeDraw];
}
@end
#import "ZHLineChartModel.h"
#import "ZHLineChartConst.h"
@interface ZHLineChartModel ()
@property (nonatomic, strong, readwrite) NSString *date;
@property (nonatomic, strong, readwrite) NSString *score;
@property (nonatomic, assign, readwrite) CGFloat yPoint;
@end
@implementation ZHLineChartModel
#pragma mark - Helper
- (void)setupYPoint {
//這步放在 model 中是因?yàn)榉奖隳P娃D(zhuǎn)換時(shí)異步計(jì)算 y 坐標(biāo)
CGFloat score = [self.score intValue];
CGFloat point = kItemTopMargin + kItemHeight / 2.f;
/**
高度計(jì)算,100-60分娃殖,按比例計(jì)算值戳,小于 60 分時(shí),小于 60 的區(qū)域按比例計(jì)算
*/
CGFloat height = 4 * (kItemHeight + kItemSpace); // 60 - 100 的總高度
if (score >= 60.f && score < 100) {
height = (100.f - score) / 40.f * height; // 按比例下降高度
point += height;
} else if (score < 60) {
// 小于 60 分炉爆,最后一段按比例計(jì)算 Y 值
CGFloat tempH = kItemHeight / 2.f + kItemSpace;
tempH = (60.f - score) / 60.f * tempH;
point += tempH + height;
}
self.yPoint = point;
}
#pragma mark - Test
+ (NSArray <ZHLineChartModel *>*)testDataArray {
NSArray *dateArr = @[@"08-29",@"08-30",@"08-31",@"09-01",@"09-02",@"09-03",@"09-04",@"09-05",@"09-06",@"09-07",@"09-08",@"09-09"];
NSArray *scoreArr = @[@"20",@"60",@"40",@"55",@"100",@"80",@"90",@"70",@"77",@"30",@"80",@"88"];
NSMutableArray *arr = [NSMutableArray array];
for (NSInteger i = 0; i < dateArr.count; i++) {
ZHLineChartModel *model = [[ZHLineChartModel alloc] init];
model.score = scoreArr[i];
model.date = dateArr[i];
[model setupYPoint];
[arr addObject:model];
}
return arr.copy;
}
@end
其他代碼詳見 Demo