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 = [[_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

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容