1、前言
最近项目中需要用到一个折线图来直观的展示已有的量化数据,也好让用户直观的看清最近数据的走势。为了满足公司设计的要求和应对以后的不可预料的需求变更,所以就花时间自己封装一个轻便折线图,后续维护起来也更方便一些。这里也就记录一下一个通用折线图的封装实现过程(下载地址在最后),通过传入不同设置以此控制内容、颜色、文本、排版等自定义属性来展示出不同的风格。下面为最终效果展示:
2、实现过程
通过上面的预览图可以看出这个折线图的共性,主要可以拆分为纵轴文本、横轴分割线、底部分割线刻度、底部文本、关键点及关键点数据、折线、颜色渐变。通过将这些元素组合在一起,并通过一些属性控制即可得到一个完整的折线图的控件。下面就开始逐步实现:
2.1纵轴文本
这里为了方便一些,折线图控件中的所有文本展示我都用label直接进行展示了,没用绘制文本的方式。纵轴文本的展示也没什么特别的地方,控制好布局即可,数量根据属性参数splitCount来控制。
for (int i = 0; i < self.splitCount + 1; i ++) {
//创建纵轴文本
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线宽和填充颜色实现不同的分割线的绘制。
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底部分割线刻度
分割线刻度绘制实现和分割线绘制类似,基本原理也是绘制线条,只是控制成多个短一些的线条进行组合,通过UIBezierPath的appendPath方法进行拼接即可。
//创建刻度
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底部文本
底部文本实现和纵轴文本基本类似,主要也是控制好布局及可,只是多一个旋转来避免文本过长展示重叠的问题。
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];
//旋转
bottomLabel.transform = CGAffineTransformMakeRotation(self.angle);
}
这里旋转通过CGAffineTransformMakeRotation方法来实现,通过传入相应的角度即可完成旋转。旋转的弧度参考坐标:
2.5关键点及关键点数据
关键点及关键点数据展示主要是计算出关键点的坐标,通过得到一个关键点的坐标即可进行一个关键点圆形绘制及关键点数据的展示实现。
- 计算关键点
关键点坐标通过计算展示高度和最大最小值之差相除得到一个比例,通过底部y值减去这个比例和关键点减最小值之差相乘的数值计算就可以得到一个关键点的y值,x值通过逐步偏移相应宽度及可计算出各个点的x值,以此即可得出相应的坐标点。
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);
//构造关键点
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];
}
}
- 关键点圆形绘制及关键点数据
通过UIBezierPath的圆形绘制方法可方便快捷的进行绘制,半径、线宽、线条颜色、填充颜色可通过属性进行控制。关键点数据的展示也是通过label直接展示,也是控制好布局及样式即可,可通过属性进行控制是否展示。
/**
* 绘制关键点及关键点数据
*/
- (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];
//关键点绘制
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];
//关键点数据
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折线
折线绘制这里提供两种方式,一种是无曲线线条直接绘制,一种是通过三次贝塞尔曲线绘制实现一个有弧度的曲线。线条直接绘制和前面的刻度绘制实现基本一致,只是由不相连的直线变成首尾相接的直线而已,这里便不再过多赘述了。这里就提一下三次贝塞尔曲线绘制,通过三次贝塞尔曲线的加持可以让线条变的有弧度,变的比较自然一些,显得不那么生硬。当然也有二次贝塞尔曲线绘制,这里主要是使用了三次贝塞尔曲线来进行绘制,毕竟控制点越多弧度可以控制的更自然一些了,这里主要使用这个方法绘制三次贝塞尔曲线:
- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2
这个方法绘制三次贝塞尔曲线。曲线段在当前点开始,在指定的点结束,controlPoint1、controlPoint2两个控制点进行弯曲程度及方向的控制。下图显示了控制点和起止点的关系以及中间发挥的作用:
开始绘制
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];
/**
* 曲线的弯曲水平。优值区间约为0.6 ~ 0.8。默认值和推荐值是0.7。
*/
@property (nonatomic) CGFloat contractionFactor;
/**
* 正常折线绘制
* 必须将CGPoint结构体包装成NSValue对象并且至少一个点来画折线。
*/
- (void)addNormalBezierThroughPoints:(NSArray *)pointArray;
/**
* 三次贝塞尔曲线绘制折线
* 必须将CGPoint结构体包装成NSValue对象并且至少一个点来画曲线。
*/
- (void)addBezierThroughPoints:(NSArray *)pointArray;
2.7颜色渐变
颜色渐变通过主要CAGradientLayer类来实现,通过colorLayer.colors赋值不同的颜色数组来控制进行哪些颜色的渐变。通过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、调用示例
开始绘制
[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、控制属性
这里通过放开一些设置属性,以此实现风格、内容、颜色、文本、排版等自定义设置,增强其通用性。
/** 折线关键点用来显示的数据 */
@property (nonatomic, strong) NSArray <NSNumber *> *lineDataAry;
/** 底部横向显示文字 */
@property (nonatomic, strong) NSArray <NSString *> *horizontalDataArr;
/** 纵轴最大值 */
@property (nonatomic, strong) NSNumber *max;
/** 纵轴最小值 */
@property (nonatomic, strong) NSNumber *min;
/** Y轴分割个数*/
@property (nonatomic, assign) NSUInteger splitCount;
/** 关键点圆半径(默认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;
/** 关键点数据文本显示宽度(默认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;
/** 底部文本旋转角度(默认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;
/** 关键点边框颜色(默认0x428eda) */
@property (nonatomic, strong) UIColor *circleStrokeColor;
/** 关键点填充颜色(默认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;
/** 关键点居中显示(默认YES) */
@property (nonatomic, assign) BOOL toCenter;
/** toCenter=YES时是否补充前后显示(默认NO) */
@property (nonatomic, assign) BOOL supplement;
/** 折线关键点数据是否显示(默认YES) */
@property (nonatomic, assign) BOOL showLineData;
/** 是否填充颜色渐变(默认YES) */
@property (nonatomic, assign) BOOL showColorGradient;
/** 渐变颜色集合 (默认0.4 0x428eda + 0.1 whiteColor)*/
@property (nonatomic, strong) NSArray *colorArr;
/**
* 渲染折线图(传参后调用才会生效)
*/
- (void)drawLineChart;
5、总结
自此一个通用的折线图控件也就算完成了,总结起来封装中也没用到太多复杂的知识点,基本上都是一些功能的基本应用,更多的是一个整合。希望此篇文章对你有所帮助,有问题或者更好的建议欢迎在下面评论提出。
下载地址:ZHLineChart,如果感觉对你有所帮助的话记得给个star喽!