iOS 封装一个折线图控件

1、前言

最近项目中需要用到一个折线图来直观的展示已有的量化数据,也好让用户直观的看清最近数据的走势。为了满足公司设计的要求和应对以后的不可预料的需求变更,所以就花时间自己封装一个轻便折线图,后续维护起来也更方便一些。这里也就记录一下一个通用折线图的封装实现过程(下载地址在最后),通过传入不同设置以此控制内容、颜色、文本、排版等自定义属性来展示出不同的风格。下面为最终效果展示:

line1.png

line2.png
line3.png
line4.png
line5.png

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方法来实现,通过传入相应的角度即可完成旋转。旋转的弧度参考坐标:


ARC
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两个控制点进行弯曲程度及方向的控制。下图显示了控制点和起止点的关系以及中间发挥的作用:


point.png

开始绘制

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喽!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 见:阅读《断舍离》已经一周多了,最开始断断续续的,到今天的收官。中途自己收拾了衣柜一次,今天收拾了书桌一次。 感:...
    你最美的公主_1f90阅读 223评论 0 2
  • 家,平均月网单30个。【4月度成果&温馨时刻】一、健康:①②早睡30天平均每日22:44分上床休息,睡眠质量70%...
    佛系的女孩阅读 222评论 0 0
  • 我还要等多久才足够 足够打败自己的对手 我还要多努力才能够 能够在众人之中出头 谁都渴望着有朝一日 像太阳一样的发...
    土豆先生的斗志昂扬阅读 703评论 2 9