前言:日常开发过程中,经常碰到数据图表类的需求,大部分UI设计会给出一套很漂亮的设计稿,如果选择使用第三方图表库,很多时候很难满足UI设计的需求,与其研究第三方库,不如花点时间自己动手写一个,自己写的代码,可以近100%的还原UI设计稿,何乐而不为?
需求:给定一组数据(x,y),要求画出一个曲线图,可以明确看出数据的变化趋势,并添加点击响应事件,查看具体数值,对应的y轴可以是正数,小数,百分比,正负数。
效果预览:
思路:
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 = [[_arrayY valueForKeyPath:@"@max.floatValue"] floatValue];
//处理最小值
minValue = [[_arrayY valueForKeyPath:@"@min.floatValue"] floatValue];
if (minValue < 0) {
minValue = 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 = [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];
CGPoint OriginPoint = 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
CGPoint originP = [[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<newPointArray.count;i++ ) {
if (i != 0) {
CGPoint prePoint = [[newPointArray objectAtIndex:i-1] CGPointValue];
CGPoint nowPoint = [[newPointArray objectAtIndex:i] CGPointValue];
[beizer addCurveToPoint:nowPoint controlPoint1:CGPointMake((nowPoint.x+prePoint.x)/2, prePoint.y) controlPoint2:CGPointMake((nowPoint.x+prePoint.x)/2, nowPoint.y)];
[bezier1 addCurveToPoint:nowPoint controlPoint1:CGPointMake((nowPoint.x+prePoint.x)/2, prePoint.y) controlPoint2:CGPointMake((nowPoint.x+prePoint.x)/2, nowPoint.y)];
if (i == newPointArray.count-1) {
[beizer moveToPoint:nowPoint];//添加连线
lastPoint = nowPoint;
}
}
}
/*遮罩*/
CGFloat bgViewHeight = self.bgScrollView.bounds.size.height;
//获取最后一个点的X值
CGFloat lastPointX = lastPoint.x;
//最后一个点对应的X轴的值
CGPoint lastPointX1 = CGPointMake(lastPointX, bgViewHeight);
[bezier1 addLineToPoint:lastPointX1];
//回到原点
[bezier1 addLineToPoint:CGPointMake(p1.x, bgViewHeight)];
[bezier1 addLineToPoint:p1];
//遮罩层
CAShapeLayer *shadeLayer = [CAShapeLayer layer];
shadeLayer.path = bezier1.CGPath;
shadeLayer.fillColor = [UIColor greenColor].CGColor;
//渐变图层
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = CGRectMake(5, 0, 0, self.bgScrollView.bounds.size.height- 50 - 60 - 50);
gradientLayer.startPoint = CGPointMake(0, 0);
gradientLayer.endPoint = CGPointMake(0, 1);
gradientLayer.cornerRadius = 5;
gradientLayer.masksToBounds = YES;
gradientLayer.colors = @[(__bridge id)[UIColor colorWithHex:0x81aeff alpha:0.25].CGColor,(__bridge id)[UIColor colorWithHex:0xb0ccff alpha:0.25].CGColor,(__bridge id)[UIColor colorWithHex:0xffffff alpha:0.25].CGColor];
gradientLayer.locations = @[@(0.33f),@(0.66f),@(1.00f)];
CALayer *baseLayer = [CALayer layer];
[baseLayer addSublayer:gradientLayer];
[baseLayer setMask:shadeLayer];
[self.bgScrollView.layer addSublayer:baseLayer];
CABasicAnimation *anmi1 = [CABasicAnimation animation];
anmi1.keyPath = @"bounds";
anmi1.duration = 1.0f;
anmi1.toValue = [NSValue valueWithCGRect:CGRectMake(5, 0, 2*lastPoint.x, self.bgScrollView.bounds.size.height)];
anmi1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
anmi1.fillMode = kCAFillModeForwards;
anmi1.autoreverses = NO;
anmi1.removedOnCompletion = NO;
[gradientLayer addAnimation:anmi1 forKey:@"bounds"];
//*****************添加动画连线******************//
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = beizer.CGPath;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.strokeColor = [UIColor colorWithHex:color].CGColor;
shapeLayer.lineWidth = 4.0f;
[self.bgScrollView.layer addSublayer:shapeLayer];
CABasicAnimation *anmi = [CABasicAnimation animation];
anmi.keyPath = @"strokeEnd";
anmi.fromValue = [NSNumber numberWithFloat:0];
anmi.toValue = [NSNumber numberWithFloat:1.0f];
anmi.duration =1.0f;
anmi.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
anmi.autoreverses = NO;
[shapeLayer addAnimation:anmi forKey:@"stroke"];
//添加点击btn
for (UIButton * btn in btnArray) {
[self.bgScrollView addSubview:btn];
}
}
PS:这里只是贴出实现该曲线图的核心代码,纵坐标是四种类型,正数,小数,百分比,正负数,其中百分比稍微特殊一点,代码虽然粗糙,但是毕竟是自己写的,想要什么样式就改成什么样式,没有啥局限性,具体实现详见Demo。