iOS开发之自定义曲线图

前言:日常开发过程中,经常碰到数据图表类的需求,大部分UI设计会给出一套很漂亮的设计稿,如果选择使用第三方图表库,很多时候很难满足UI设计的需求,与其研究第三方库,不如花点时间自己动手写一个,自己写的代码,可以近100%的还原UI设计稿,何乐而不为?
需求:给定一组数据(x,y),要求画出一个曲线图,可以明确看出数据的变化趋势,并添加点击响应事件,查看具体数值,对应的y轴可以是正数,小数,百分比,正负数。
效果预览:

preview.gif
思路:
1.因为是在手机上显示,所以这里选择将y轴做一个n等分,在给定数据中(本demo是模拟20组数据)取出最大值,计算出y轴刻度;
2.横坐标一般都是日期,给一个固定间隔,用UILabel显示出来就好;
3.根据每一组数据的y值与最大值的比例,计算每一组数据在坐标系中的位置,并记录每一数据在坐标系中的点;
4.在每一个点的位置创建一个UIButton,用于点击响应事件;
5.利用UIBezierPath+CAGradientLayer+CABasicAnimation完成曲线图的动态绘制,并添加遮罩层;
实现:
这里主要介绍构建xy坐标系,获取对应点,动态完成曲线绘制,详细实现可自行下载Demo。
1.首先确定y轴,这里选择将y轴8等分,也就是说y轴上有9个刻度,一般是从0开始,以max_y/8(max_y是所有数据中y轴的最大值)的刻度递增:

//灰色背景线条 + 纵坐标 -(void)initBaseLineViewWithArrayY:(NSMutableArray *)array{for (int i = 0; i < baseLineCount; i++) { //灰色背景线条 UIImageView * lineView = [[UIImageView alloc]init]; lineView.backgroundColor = [UIColor colorWithHex:0xE2EBF2]; [self.bgScrollView addSubview:lineView]; if (i == baseLineCount -1) { lastLineView = lineView; } //纵坐标 UILabel * labelY = [[UILabel alloc]init]; labelY.textAlignment = NSTextAlignmentCenter; labelY.textColor = [UIColor colorWithHex:0x999999]; labelY.font = [UIFont systemFontOfSize:11]; if (minValue < 0) { if (i <4) { //y坐标间隔 int interval = maxValue/4.0; labelY.text = [NSString stringWithFormat:@"%ld",((baseLineCount-4) - i -1)*interval]; } if (i == 4) { labelY.text = @"0"; } if (i > 4) { //y坐标间隔 int interval = -minValue/4.0; labelY.text = [NSString stringWithFormat:@"%ld",((baseLineCount-4) - i -1)*interval]; } }else{//y坐标间隔 if (_type == 12) { CGFloat interval = maxValue/8.0; labelY.text = [NSString stringWithFormat:@"%.2f",(baseLineCount - i -1)*interval]; }else{ int interval = maxValue/8.0; labelY.text = [NSString stringWithFormat:@"%ld",(baseLineCount - i -1)*interval]; } } [self addSubview:labelY]; [lineView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.bgScrollView.mas_top).with.offset(8+i*23); make.left.equalTo(self.bgScrollView.mas_left).with.offset(0.0f); make.right.equalTo(self.mas_right).with.offset(22.0f); make.height.equalTo(@0.5f); }]; [labelY mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.mas_left).with.offset(0.0f); make.right.equalTo(self.bgScrollView.mas_left).with.offset(5.0f); make.centerY.equalTo(lineView); make.height.equalTo(@17.0f); }]; } }

2.x轴坐标相对简单,因为只有日期,等间隔显示就行:
//横坐标 -(void)initXLineWithArrayX:(NSMutableArray *)array{ //横坐标 for (int i = 0; i < array.count; i++) {UILabel * labelX = [[UILabel alloc]init]; labelX.textAlignment = NSTextAlignmentCenter; labelX.textColor = [UIColor colorWithHex:0x999999]; labelX.font = [UIFont systemFontOfSize:12]; labelX.text = [NSString stringWithFormat:@"%@",array[i]]; CGAffineTransform transform = CGAffineTransformIdentity; transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 40); transform = CGAffineTransformTranslate(transform, 20, 0); labelX.layer.affineTransform = transform; [self.bgScrollView addSubview:labelX]; [labelX mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.bgScrollView.mas_left).with.offset(i * (45)); make.top.equalTo(lastLineView.mas_bottom).with.offset(14.0f); make.width.greaterThanOrEqualTo(@0.0f); make.height.equalTo(@17.0); }]; [xLabelArray addObject:labelX]; } }

3.数据处理(将x轴,y轴分开处理),提取最值,设置默认偏移量等:
-(void)setArrayX:(NSMutableArray *)arrayX{_arrayX = arrayX; }-(void)setArrayY:(NSMutableArray *)arrayY{_arrayY = arrayY; //处理最大值 maxValue = https://www.it610.com/article/[[_arrayY valueForKeyPath:@"@max.floatValue"] floatValue]; //处理最小值 minValue = https://www.it610.com/article/[[_arrayY valueForKeyPath:@"@min.floatValue"] floatValue]; if (minValue < 0) { minValue = https://www.it610.com/article/minValue - 1; int minRemainder = (int)(-minValue)%4; minValue = minValue + (minRemainder - 4); //+1 消除浮点型数据带来的误差 maxValue = maxValue + 1; //对4取余数 int remainder = (int)maxValue%4; //确保maxValue能被8整除 maxValue = maxValue + (4 - remainder); }else{ //当绩效比最大值小于0.1, maxValue = 0.1 if (_type == 12) {if (maxValue < 0.1) { maxValue = 0.1; } }else{ //+1 消除浮点型数据带来的误差 maxValue = maxValue + 1; //对8取余数 int remainder = (int)maxValue%8; //确保maxValue能被8整除 maxValue = maxValue + (8 - remainder); } } //背景线条 + Y轴 [self initBaseLineViewWithArrayY:nil]; //X轴 [self initXLineWithArrayX:_arrayX]; self.bgScrollView.contentSize = CGSizeMake( (_arrayX.count)*50, self.bounds.size.height - 60); self.bgScrollView.contentOffset = CGPointMake((_arrayX.count)*50 - CGRectGetWidth(self.bounds), 0); //获取拐点 [self getInflectionPointWithArrayX:nil ArrayY:_arrayY color:0x4162FF]; }

4.获取每组数据在xy坐标系中的位置(点),并绘制曲线图,添加动画:
//获取拐点 -(void)getInflectionPointWithArrayX:(NSMutableArray *)xArray ArrayY:(NSMutableArray *)yArray color:(long)color{[self layoutIfNeeded]; NSMutableArray * midPointArray = [NSMutableArray arrayWithCapacity:0]; for (UILabel * label in xLabelArray) { NSValue *point = [NSValue valueWithCGPoint:CGPointMake(label.center.x, 0)]; [midPointArray addObject:point]; } for (int i = 0; i < yArray.count; i++) {CGFloat currentData = https://www.it610.com/article/[yArray[i] floatValue]; CGFloat possionY = 0; if (minValue < 0) { if (currentData>= 0) { possionY = (92/maxValue)*(maxValue - currentData) + 8; }else{ possionY = 92 + (92/(minValue))*(currentData) + 8; }}else{ possionY = (184/maxValue)*(maxValue - currentData) + 8; }NSValue * point = midPointArray[i]; CGPointOriginPoint = point.CGPointValue; OriginPoint.y = possionY; NSValue * newPoint = [NSValue valueWithCGPoint:OriginPoint]; [pointArray addObject:newPoint]; //点击buton UIButton * pointBtn = [UIButton buttonWithType:UIButtonTypeCustom]; pointBtn.frame = CGRectMake(0, 0, 30, 30); [pointBtn setImage:[UIImage imageNamed:@"dpoint_blue"] forState:UIControlStateNormal]; [pointBtn setImage:[UIImage imageNamed:@"dpoint_blue_select"] forState:UIControlStateSelected]; [pointBtn addTarget:self action:@selector(clickButtonAction:) forControlEvents:UIControlEventTouchUpInside]; pointBtn.center = OriginPoint; pointBtn.tag = i + 1000; [btnArray addObject:pointBtn]; UIButton * messageBtn = [UIButton buttonWithType:UIButtonTypeCustom]; messageBtn.frame = CGRectMake(0, 0, 65, 39); messageBtn.tag = i + 2000; messageBtn.hidden = YES; messageBtn.titleLabel.font = [UIFont systemFontOfSize:12]; messageBtn.userInteractionEnabled = NO; messageBtn.titleLabel.adjustsFontSizeToFitWidth = YES; //大于-1小于1,保留四位小数,大于0.01,保留2位小数 if ([_arrayY[i] floatValue] < 1 &&[_arrayY[i] floatValue] > -1) {[messageBtn setTitle:[NSString stringWithFormat:@" %@ ",[Helper notRounding:_arrayY[i] afterPoint:4]] forState:UIControlStateNormal]; }else{ [messageBtn setTitle:[NSString stringWithFormat:@" %@ ",[Helper notRounding:_arrayY[i] afterPoint:2]] forState:UIControlStateNormal]; } if ([yArray[i] floatValue] > maxValue/2.0f) {[messageBtn setBackgroundImage:[UIImage imageNamed:@"icon_message_top"] forState:UIControlStateNormal]; messageBtn.titleEdgeInsets = UIEdgeInsetsMake(0, 0, -8, 0); messageBtn.center = CGPointMake(OriginPoint.x, OriginPoint.y + 25); }else{[messageBtn setBackgroundImage:[UIImage imageNamed:@"icon_message_down"] forState:UIControlStateNormal]; messageBtn.titleEdgeInsets = UIEdgeInsetsMake(0, 0, 8, 0); messageBtn.center = CGPointMake(OriginPoint.x, OriginPoint.y-25); }[labelArray addObject:messageBtn]; }//曲线 //起点往前偏移17.5 CGPointoriginP = [[pointArray objectAtIndex:0] CGPointValue]; CGPoint p1 = CGPointMake(originP.x - 27.5, originP.y); //CGPoint p1 = [[pointArray objectAtIndex:0] CGPointValue]; NSMutableArray * newPointArray = [NSMutableArray arrayWithArray:pointArray]; NSValue *point = [NSValue valueWithCGPoint:p1]; [newPointArray insertObject:point atIndex:0]; //直线的连线 UIBezierPath *beizer = [UIBezierPath bezierPath]; //beizer. [beizer moveToPoint:p1]; /*遮罩*/ UIBezierPath *bezier1 = [UIBezierPath bezierPath]; bezier1.lineCapStyle = kCGLineCapRound; bezier1.lineJoinStyle = kCGLineJoinMiter; [bezier1 moveToPoint:p1]; for (int i = 0; i

【iOS开发之自定义曲线图】PS:这里只是贴出实现该曲线图的核心代码,纵坐标是四种类型,正数,小数,百分比,正负数,其中百分比稍微特殊一点,代码虽然粗糙,但是毕竟是自己写的,想要什么样式就改成什么样式,没有啥局限性,具体实现详见Demo。

    推荐阅读