1服鹅、前言
最近項目中需要用到一個折線圖來直觀的展示已有的量化數(shù)據(jù)混蔼,也好讓用戶直觀的看清最近數(shù)據(jù)的走勢涧尿。為了滿足公司設(shè)計的要求和應(yīng)對以后的不可預(yù)料的需求變更系奉,所以就花時間自己封裝一個輕便折線圖,后續(xù)維護起來也更方便一些姑廉。這里也就記錄一下一個通用折線圖的封裝實現(xiàn)過程(下載地址在最后)缺亮,通過傳入不同設(shè)置以此控制內(nèi)容、顏色桥言、文本萌踱、排版等自定義屬性來展示出不同的風(fēng)格。下面為最終效果展示:
2号阿、實現(xiàn)過程
通過上面的預(yù)覽圖可以看出這個折線圖的共性并鸵,主要可以拆分為縱軸文本、橫軸分割線、底部分割線刻度、底部文本疆瑰、關(guān)鍵點及關(guān)鍵點數(shù)據(jù)坡锡、折線枫慷、顏色漸變序调。通過將這些元素組合在一起涣仿,并通過一些屬性控制即可得到一個完整的折線圖的控件赦颇。下面就開始逐步實現(xiàn):
2.1縱軸文本
這里為了方便一些咏闪,折線圖控件中的所有文本展示我都用label直接進行展示了曙搬,沒用繪制文本的方式「肷縱軸文本的展示也沒什么特別的地方纵装,控制好布局即可,數(shù)量根據(jù)屬性參數(shù)splitCount來控制溪胶。
for (int i = 0; i < self.splitCount + 1; i ++) {
//創(chuàng)建縱軸文本
UILabel *leftLabel = [[UILabel alloc] init];
leftLabel.frame = CGRectMake(self.edge.left, self.leftTextWidth + (spaceY + labelHeight) * i, self.leftTextWidth, labelHeight);
leftLabel.textColor = self.textColor;
leftLabel.textAlignment = NSTextAlignmentRight;
leftLabel.font = [UIFont systemFontOfSize:self.textFontSize];
NSInteger leftNum = self.max.integerValue - numSpace * i;
if (i == self.splitCount) {
leftNum = self.min.integerValue;
}
leftLabel.text = [NSString stringWithFormat:@"%ld",leftNum];
[self addSubview:leftLabel];
}
2.2橫軸分割線
分割線直接用UIBezierPath通過傳入計算好的控制點直接繪制即可搂擦,通過改變CAShapeLayer線寬和填充顏色實現(xiàn)不同的分割線的繪制。
UIBezierPath *linePath = [UIBezierPath bezierPath];
CGFloat minX = CGRectGetMaxX(leftLabel.frame) + self.lineToLeftOffset;
CGFloat maxX = CGRectGetMaxX(self.frame) - self.edge.right;
[linePath moveToPoint:CGPointMake(minX, CGRectGetMidY(leftLabel.frame))];
[linePath addLineToPoint:CGPointMake(maxX, CGRectGetMidY(leftLabel.frame))];
hLineLayer.strokeColor = self.horizontalLineColor.CGColor;
hLineLayer.lineWidth = self.horizontalLineWidth;
hLineLayer.strokeColor = self.horizontalBottomLineColor.CGColor;
hLineLayer.lineWidth = self.horizontalBottomLineWidth;
2.3底部分割線刻度
分割線刻度繪制實現(xiàn)和分割線繪制類似哗脖,基本原理也是繪制線條瀑踢,只是控制成多個短一些的線條進行組合,通過UIBezierPath的appendPath方法進行拼接即可才避。
//創(chuàng)建刻度
UIBezierPath *bezierPath = [UIBezierPath bezierPath];
NSInteger count = self.horizontalDataArr.count;
if (self.toCenter) {
count = self.horizontalDataArr.count + 1;
}
for (int j = 0 ; j < count; j ++) {
[bezierPath moveToPoint:CGPointMake(minX + spaceX * j, maxMidY + self.scaleOffset)];
[bezierPath addLineToPoint:CGPointMake(minX + spaceX * j, maxMidY + 2 + self.scaleOffset)];
[linePath appendPath:bezierPath];
}
2.4底部文本
底部文本實現(xiàn)和縱軸文本基本類似橱夭,主要也是控制好布局及可,只是多一個旋轉(zhuǎn)來避免文本過長展示重疊的問題桑逝。
for (int k = 0; k < self.horizontalDataArr.count; k ++) {
CGFloat midX = minX + (spaceX * k) + (self.toCenter ? spaceX / 2 : 0);
UILabel *bottomLabel = [[UILabel alloc] init];
bottomLabel.frame = CGRectMake(midX - bottomLabelWidth / 2, maxMidY + self.bottomOffset,
bottomLabelWidth, labelHeight);
bottomLabel.textColor = self.textColor;
bottomLabel.textAlignment = NSTextAlignmentCenter;
bottomLabel.font = [UIFont systemFontOfSize:self.textFontSize];
bottomLabel.text = self.horizontalDataArr[k];
[self addSubview:bottomLabel];
//旋轉(zhuǎn)
bottomLabel.transform = CGAffineTransformMakeRotation(self.angle);
}
這里旋轉(zhuǎn)通過CGAffineTransformMakeRotation方法來實現(xiàn)棘劣,通過傳入相應(yīng)的角度即可完成旋轉(zhuǎn)。旋轉(zhuǎn)的弧度參考坐標:
2.5關(guān)鍵點及關(guān)鍵點數(shù)據(jù)
關(guān)鍵點及關(guān)鍵點數(shù)據(jù)展示主要是計算出關(guān)鍵點的坐標楞遏,通過得到一個關(guān)鍵點的坐標即可進行一個關(guān)鍵點圓形繪制及關(guān)鍵點數(shù)據(jù)的展示實現(xiàn)茬暇。
- 計算關(guān)鍵點
關(guān)鍵點坐標通過計算展示高度和最大最小值之差相除得到一個比例,通過底部y值減去這個比例和關(guān)鍵點減最小值之差相乘的數(shù)值計算就可以得到一個關(guān)鍵點的y值寡喝,x值通過逐步偏移相應(yīng)寬度及可計算出各個點的x值糙俗,以此即可得出相應(yīng)的坐標點。
CGFloat ratio = (maxMidY - minMidY) / (self.max.floatValue - self.min.floatValue);
for (int k = 0; k < self.horizontalDataArr.count; k ++) {
CGFloat midX = minX + (spaceX * k) + (self.toCenter ? spaceX / 2 : 0);
//構(gòu)造關(guān)鍵點
NSNumber *tempNum = self.lineDataAry[k];
CGFloat y = maxMidY - (tempNum.integerValue - self.min.floatValue) * ratio;
if (self.toCenter && self.supplement && !k) {
NSValue *value = [NSValue valueWithCGPoint:CGPointMake(minX, y)];
[pointArr addObject:value];
}
NSValue *value = [NSValue valueWithCGPoint:CGPointMake(midX, y)];
[pointArr addObject:value];
if (self.toCenter && self.supplement && k == self.lineDataAry.count - 1) {
NSValue *value = [NSValue valueWithCGPoint:CGPointMake(maxX, y)];
[pointArr addObject:value];
}
}
- 關(guān)鍵點圓形繪制及關(guān)鍵點數(shù)據(jù)
通過UIBezierPath的圓形繪制方法可方便快捷的進行繪制预鬓,半徑巧骚、線寬、線條顏色格二、填充顏色可通過屬性進行控制劈彪。關(guān)鍵點數(shù)據(jù)的展示也是通過label直接展示,也是控制好布局及樣式即可顶猜,可通過屬性進行控制是否展示沧奴。
/**
* 繪制關(guān)鍵點及關(guān)鍵點數(shù)據(jù)
*/
- (void)buildDotWithPointsArr:(NSMutableArray *)pointsArr
{
for (int i = 0; i < pointsArr.count; i ++) {
if (self.toCenter && self.supplement && (!i || i == pointsArr.count - 1)) {
continue;
}
NSValue *point = pointsArr[i];
//關(guān)鍵點繪制
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(point.CGPointValue.x, point.CGPointValue.y) radius:self.circleRadius startAngle:0 endAngle:M_PI * 2 clockwise:NO];
CAShapeLayer *circleLayer = [CAShapeLayer layer];
circleLayer.path = path.CGPath;
circleLayer.strokeColor = self.circleStrokeColor.CGColor;
circleLayer.fillColor = self.circleFillColor.CGColor;
circleLayer.lineWidth = self.lineWidth;
circleLayer.lineCap = kCALineCapRound;
circleLayer.lineJoin = kCALineJoinRound;
circleLayer.contentsScale = [UIScreen mainScreen].scale;
[self.layer addSublayer:circleLayer];
//關(guān)鍵點數(shù)據(jù)
if (self.showLineData) {
UILabel *numLabel = [[UILabel alloc] init];
numLabel.frame = CGRectMake(point.CGPointValue.x - self.dataTextWidth / 2, point.CGPointValue.y - 18, self.dataTextWidth, self.textFontSize);
numLabel.textColor = self.textColor;
numLabel.textAlignment = NSTextAlignmentRight;
numLabel.font = [UIFont systemFontOfSize:self.textFontSize];
NSInteger index = i;
if (self.toCenter && self.supplement) {
index = i - 1;
}
numLabel.text = [NSString stringWithFormat:@"%@",self.lineDataAry[index]];
[self addSubview:numLabel];
}
}
}
2.6折線
折線繪制這里提供兩種方式,一種是無曲線線條直接繪制长窄,一種是通過三次貝塞爾曲線繪制實現(xiàn)一個有弧度的曲線滔吠。線條直接繪制和前面的刻度繪制實現(xiàn)基本一致远寸,只是由不相連的直線變成首尾相接的直線而已,這里便不再過多贅述了屠凶。這里就提一下三次貝塞爾曲線繪制,通過三次貝塞爾曲線的加持可以讓線條變的有弧度肆资,變的比較自然一些矗愧,顯得不那么生硬。當(dāng)然也有二次貝塞爾曲線繪制郑原,這里主要是使用了三次貝塞爾曲線來進行繪制唉韭,畢竟控制點越多弧度可以控制的更自然一些了,這里主要使用這個方法繪制三次貝塞爾曲線:
- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2
這個方法繪制三次貝塞爾曲線犯犁。曲線段在當(dāng)前點開始属愤,在指定的點結(jié)束,controlPoint1酸役、controlPoint2兩個控制點進行彎曲程度及方向的控制住诸。下圖顯示了控制點和起止點的關(guān)系以及中間發(fā)揮的作用:
開始繪制
CGPoint startPoint = [[pointArr firstObject] CGPointValue];
CGPoint endPoint = [[pointArr lastObject] CGPointValue];
UIBezierPath *linePath = [UIBezierPath bezierPath];
[linePath moveToPoint:startPoint];
if (self.addCurve) {
[linePath addBezierThroughPoints:pointArr];
} else {
[linePath addNormalBezierThroughPoints:pointArr];
}
CAShapeLayer *lineLayer = [CAShapeLayer layer];
lineLayer.path = linePath.CGPath;
lineLayer.strokeColor = self.lineColor.CGColor;
lineLayer.fillColor = [UIColor clearColor].CGColor;
lineLayer.lineWidth = self.lineWidth;
lineLayer.lineCap = kCALineCapRound;
lineLayer.lineJoin = kCALineJoinRound;
lineLayer.contentsScale = [UIScreen mainScreen].scale;
[self.layer addSublayer:lineLayer];
/**
* 曲線的彎曲水平。優(yōu)值區(qū)間約為0.6 ~ 0.8涣澡。默認值和推薦值是0.7贱呐。
*/
@property (nonatomic) CGFloat contractionFactor;
/**
* 正常折線繪制
* 必須將CGPoint結(jié)構(gòu)體包裝成NSValue對象并且至少一個點來畫折線。
*/
- (void)addNormalBezierThroughPoints:(NSArray *)pointArray;
/**
* 三次貝塞爾曲線繪制折線
* 必須將CGPoint結(jié)構(gòu)體包裝成NSValue對象并且至少一個點來畫曲線入桂。
*/
- (void)addBezierThroughPoints:(NSArray *)pointArray;
2.7顏色漸變
顏色漸變通過主要CAGradientLayer類來實現(xiàn)奄薇,通過colorLayer.colors賦值不同的顏色數(shù)組來控制進行哪些顏色的漸變。通過colorLayer.startPoint和colorLayer.endPoint得到一個漸變的方向抗愁,這里是上下方式的漸變馁蒂。
//顏色漸變
if (self.showColorGradient) {
UIBezierPath *colorPath = [UIBezierPath bezierPath];
colorPath.lineWidth = 1.f;
[colorPath moveToPoint:startPoint];
if (self.addCurve) {
[colorPath addBezierThroughPoints:pointArr];
} else {
[colorPath addNormalBezierThroughPoints:pointArr];
}
[colorPath addLineToPoint:CGPointMake(endPoint.x, maxMidY)];
[colorPath addLineToPoint:CGPointMake(startPoint.x, maxMidY)];
[colorPath addLineToPoint:CGPointMake(startPoint.x, startPoint.y)];
CAShapeLayer *bgLayer = [CAShapeLayer layer];
bgLayer.path = colorPath.CGPath;
bgLayer.frame = self.bounds;
CAGradientLayer *colorLayer = [CAGradientLayer layer];
colorLayer.frame = bgLayer.frame;
colorLayer.mask = bgLayer;
colorLayer.startPoint = CGPointMake(0, 0);
colorLayer.endPoint = CGPointMake(0, 1);
colorLayer.colors = self.colorArr;
[self.layer addSublayer:colorLayer];
}
3、調(diào)用示例
開始繪制
[self.lineView drawLineChart];
初始化
- (ZHLineChartView *)lineView
{
if (!_lineView) {
_lineView = [[ZHLineChartView alloc] initWithFrame:CGRectMake(0, 10, CGRectGetWidth(self.view.frame), 200)];
_lineView.max = @600;
_lineView.min = @300;
_lineView.horizontalDataArr = @[@"2020-02", @"2020-03", @"2020-04", @"2020-05", @"2020-06", @"2020-07"];
_lineView.lineDataAry = @[@502, @523, @482, @455, @473, @546];
_lineView.splitCount = 3;
_lineView.toCenter = NO;
_lineView.edge = UIEdgeInsetsMake(25, 15, 50, 25);
[self.scrollView addSubview:_lineView];
}
return _lineView;
}
效果展示
4蜘腌、控制屬性
這里通過放開一些設(shè)置屬性沫屡,以此實現(xiàn)風(fēng)格、內(nèi)容逢捺、顏色谁鳍、文本、排版等自定義設(shè)置劫瞳,增強其通用性倘潜。
/** 折線關(guān)鍵點用來顯示的數(shù)據(jù) */
@property (nonatomic, strong) NSArray <NSNumber *> *lineDataAry;
/** 底部橫向顯示文字 */
@property (nonatomic, strong) NSArray <NSString *> *horizontalDataArr;
/** 縱軸最大值 */
@property (nonatomic, strong) NSNumber *max;
/** 縱軸最小值 */
@property (nonatomic, strong) NSNumber *min;
/** Y軸分割個數(shù)*/
@property (nonatomic, assign) NSUInteger splitCount;
/** 關(guān)鍵點圓半徑(默認3) */
@property (nonatomic, assign) CGFloat circleRadius;
/** 折線寬(默認1.5) */
@property (nonatomic, assign) CGFloat lineWidth;
/** 橫向分割線寬(默認0.5) */
@property (nonatomic, assign) CGFloat horizontalLineWidth;
/** 底部橫向分割線寬(默認1) */
@property (nonatomic, assign) CGFloat horizontalBottomLineWidth;
/** 關(guān)鍵點數(shù)據(jù)文本顯示寬度(默認20) */
@property (nonatomic, assign) CGFloat dataTextWidth;
/** 縱軸文本顯示寬度(默認25) */
@property (nonatomic, assign) CGFloat leftTextWidth;
/** 刻度上下偏移(默認0) */
@property (nonatomic, assign) CGFloat scaleOffset;
/** 底部文本上下偏移(默認20) */
@property (nonatomic, assign) CGFloat bottomOffset;
/** 橫向分割線距離左邊文本偏移距離(默認5) */
@property (nonatomic, assign) CGFloat lineToLeftOffset;
/** 底部文本旋轉(zhuǎn)角度(默認M_PI * 1.75) */
@property (nonatomic, assign) CGFloat angle;
/** 文本字號(默認10) */
@property (nonatomic, assign) CGFloat textFontSize;
/** 邊界(默認UIEdgeInsetsMake(25, 5, 40, 15)) */
@property (nonatomic, assign) UIEdgeInsets edge;
/** 關(guān)鍵點邊框顏色(默認0x428eda) */
@property (nonatomic, strong) UIColor *circleStrokeColor;
/** 關(guān)鍵點填充顏色(默認whiteColor) */
@property (nonatomic, strong) UIColor *circleFillColor;
/** 縱向橫向顯示文本顏色(默認0x666666) */
@property (nonatomic, strong) UIColor *textColor;
/** 折線顏色(默認0x428eda) */
@property (nonatomic, strong) UIColor *lineColor;
/** 橫向分割線顏色(默認0xe8e8e8) */
@property (nonatomic, strong) UIColor *horizontalLineColor;
/** 底部橫向分割線顏色(默認0x428eda) */
@property (nonatomic, strong) UIColor *horizontalBottomLineColor;
/** 貝塞爾曲線繪制,增加曲度控制(默認YES) */
@property (nonatomic, assign) BOOL addCurve;
/** 關(guān)鍵點居中顯示(默認YES) */
@property (nonatomic, assign) BOOL toCenter;
/** toCenter=YES時是否補充前后顯示(默認NO) */
@property (nonatomic, assign) BOOL supplement;
/** 折線關(guān)鍵點數(shù)據(jù)是否顯示(默認YES) */
@property (nonatomic, assign) BOOL showLineData;
/** 是否填充顏色漸變(默認YES) */
@property (nonatomic, assign) BOOL showColorGradient;
/** 漸變顏色集合 (默認0.4 0x428eda + 0.1 whiteColor)*/
@property (nonatomic, strong) NSArray *colorArr;
/**
* 渲染折線圖(傳參后調(diào)用才會生效)
*/
- (void)drawLineChart;
5志于、總結(jié)
自此一個通用的折線圖控件也就算完成了涮因,總結(jié)起來封裝中也沒用到太多復(fù)雜的知識點,基本上都是一些功能的基本應(yīng)用伺绽,更多的是一個整合养泡。希望此篇文章對你有所幫助嗜湃,有問題或者更好的建議歡迎在下面評論提出。
下載地址:ZHLineChart澜掩,如果感覺對你有所幫助的話記得給個star嘍购披!