iOS 自定义折线图

iOS 自定义折线图

效果及 Demo

由于产品需求要展示分数,并且60分以下展示需要压缩比例,而开源的图表库太过复杂并且有些无法满足需求,学习成本偏高,于是决定自己写一个,效果如下图。

Demo地址

chart-w480

视图层级及设计思路

  1. 使用UICollectionView实现滚动效果,并且部分展示元素可以复用,如下图中日期label、虚线及圆点
  2. 使用CAShapeLayer+UIBezierPath实现折线及阴影绘制
cell-w480

代码实现

图表实现代码

#import "ZHLineChartView.h"

#import "ZHLineChartConst.h"
#import "ZHLineChartModel.h"

@interface ZHLineChartView ()

@end


@implementation ZHLineChartView

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        // 这步是因为,有其他 layer 时,折线图需要展示在最底层
        self.layer.zPosition = -1.f;
    }
    return self;
}

#pragma mark - Load Subviews

- (void)makeDraw {
    // x轴的 y
    CGFloat moveStartY = kChartViewH;
    
    UIBezierPath *aPath = [UIBezierPath bezierPath];//填充镀层
    UIBezierPath *aPathLine = [UIBezierPath bezierPath];//线
    
    // 所有经过的坐标点,画线
//    NSInteger startI = 0; // 用于计算无效点阴影
    BOOL ignoreZeroPoint = YES;
    for (int i = 0; i < _dataModels.count; i++) {
        CGFloat yPoint = _dataModels[i].yPoint;
        if (ignoreZeroPoint && yPoint == kInvaildYPoint) { //忽略前面为0的点(产品需求)
            continue;
        }
        CGFloat xPoint = kLineChartMoveStartX + i * kLineChartMoveW;
        if (ignoreZeroPoint) {
            //设置开始的坐标点
            //取得第一个有效点的 index
//            startI = i;
            [aPath moveToPoint:CGPointMake(xPoint, moveStartY)]; //镀层要从 x 轴开始画,再添加第一个点坐标
            [aPath addLineToPoint:CGPointMake(xPoint, yPoint)];
            [aPathLine moveToPoint:CGPointMake(xPoint, yPoint)];
            ignoreZeroPoint = NO; //开始画点,不再忽略为0的点
        } else {
            [aPath addLineToPoint:CGPointMake(xPoint, yPoint)];
            [aPathLine addLineToPoint:CGPointMake(xPoint, yPoint)];
        }
    }
    
    //镀层收边
    CGFloat lineW = kLineChartMoveStartX + (_dataModels.count - 1) * kLineChartMoveW;
    [aPath addLineToPoint:CGPointMake(lineW, moveStartY)];
    [aPath closePath];
    
    // 前面为 0 的点灰色蒙层,UI 后来又不要了
//    CAGradientLayer *grayLayer = [CAGradientLayer layer];
//    grayLayer.colors = @[(__bridge id)[[UIColor lightGrayColor] colorWithAlphaComponent:0.f].CGColor, (__bridge id)[[UIColor lightGrayColor] colorWithAlphaComponent:0.4f].CGColor];
//    grayLayer.frame = CGRectMake(kLineChartMoveStartX, 0.f, startI * kLineChartMoveW, moveStartY);
//    grayLayer.startPoint = CGPointZero;
//    grayLayer.endPoint = CGPointMake(1.f, 1.f);
//    [self.layer addSublayer:grayLayer];
    
    
    [self addGradientLayerWithPath:aPath];
    [self addLineLayerWithPath:aPathLine];
    [self addXLineLayer];
}

- (void)addXLineLayer {
    CGFloat lineW = kLineChartMoveStartX + (_dataModels.count - 1) * kLineChartMoveW;
    //x 轴
    UIBezierPath *xLinePath = [UIBezierPath bezierPath];
    [xLinePath moveToPoint:CGPointMake(kLineChartMoveStartX - kLineChartMoveW / 2.f, kChartViewH)];
    [xLinePath addLineToPoint:CGPointMake(kLineChartMoveStartX + lineW, kChartViewH)];
    
    CAShapeLayer *xLineLayer = [CAShapeLayer layer];
    xLineLayer.strokeColor = [UIColor lightGrayColor].CGColor;
    xLineLayer.lineWidth = 0.5f;
    xLineLayer.path = xLinePath.CGPath;
    xLineLayer.zPosition = -2;
    [self.layer addSublayer:xLineLayer];
}

- (void)addLineLayerWithPath:(UIBezierPath *)path {
    //折线
    CAShapeLayer *shapelayerLine = [CAShapeLayer layer];
    //设置边框颜色,就是上边画的,线的颜色
    shapelayerLine.strokeColor = [[UIColor orangeColor] CGColor];
    //设置填充颜色 如果不需要[UIColor clearColor]
    shapelayerLine.fillColor = [[UIColor clearColor] CGColor];
    //就是这句话在关联彼此(UIBezierPath和CAShapeLayer):
    shapelayerLine.path = path.CGPath;
    [self.layer addSublayer:shapelayerLine];
}

- (void)addGradientLayerWithPath:(UIBezierPath *)path {
    //获取总共的长度
    CGFloat lineW = kLineChartMoveStartX + (_dataModels.count - 1) * kLineChartMoveW;
    
    CAShapeLayer *shapelayer = [CAShapeLayer layer];
    //设置边框颜色,就是上边画的,线的颜色
    shapelayer.strokeColor = [[UIColor orangeColor] CGColor];
    //设置填充颜色
    shapelayer.fillColor = [[UIColor orangeColor] CGColor];
    //就是这句话在关联彼此(UIBezierPath和CAShapeLayer):
    shapelayer.path = path.CGPath;
    
    CAGradientLayer *gradientLayer = [CAGradientLayer layer];
    gradientLayer.colors = @[(__bridge id)[[UIColor orangeColor] colorWithAlphaComponent:0.4f].CGColor, (__bridge id)[[UIColor orangeColor] colorWithAlphaComponent:0.f].CGColor];
    //    gradientLayer.locations = @[@0.f, @0.5f];
    gradientLayer.startPoint = CGPointMake(0.5, 0.f);
    gradientLayer.endPoint = CGPointMake(0.5, 1.f);
    gradientLayer.shadowPath = path.CGPath;
    gradientLayer.frame = CGRectMake(0, 0, lineW, kChartViewH);
    gradientLayer.mask = shapelayer;
    [self.layer addSublayer:gradientLayer];
}

#pragma mark - Setter

- (void)setDataModels:(NSArray<ZHLineChartModel *> *)dataModels {
    _dataModels = dataModels;
    
    [self makeDraw];
}


@end
#import "ZHLineChartModel.h"

#import "ZHLineChartConst.h"

@interface ZHLineChartModel ()

@property (nonatomic, strong, readwrite) NSString *date;
@property (nonatomic, strong, readwrite) NSString *score;
@property (nonatomic, assign, readwrite) CGFloat yPoint;

@end

@implementation ZHLineChartModel

#pragma mark - Helper

- (void)setupYPoint {
    //这步放在 model 中是因为方便模型转换时异步计算 y 坐标
    CGFloat score = [self.score intValue];
    CGFloat point = kItemTopMargin + kItemHeight / 2.f;
    
    /**
     高度计算,100-60分,按比例计算,小于 60 分时,小于 60 的区域按比例计算
     */
    CGFloat height = 4 * (kItemHeight + kItemSpace); // 60 - 100 的总高度
    if (score >= 60.f && score < 100) {
        height = (100.f - score) / 40.f * height; // 按比例下降高度
        point += height;
    } else if (score < 60) {
        // 小于 60 分,最后一段按比例计算 Y 值
        CGFloat tempH = kItemHeight / 2.f + kItemSpace;
        tempH = (60.f - score) / 60.f * tempH;
        point += tempH + height;
    }
    
    self.yPoint = point;
}


#pragma mark - Test

+ (NSArray <ZHLineChartModel *>*)testDataArray {
    NSArray *dateArr = @[@"08-29",@"08-30",@"08-31",@"09-01",@"09-02",@"09-03",@"09-04",@"09-05",@"09-06",@"09-07",@"09-08",@"09-09"];
    NSArray *scoreArr = @[@"20",@"60",@"40",@"55",@"100",@"80",@"90",@"70",@"77",@"30",@"80",@"88"];
    
    NSMutableArray *arr = [NSMutableArray array];
    
    for (NSInteger i = 0; i < dateArr.count; i++) {
        ZHLineChartModel *model = [[ZHLineChartModel alloc] init];
        model.score = scoreArr[i];
        model.date = dateArr[i];
        [model setupYPoint];
        
        [arr addObject:model];
    }
    
    return arr.copy;
}

@end

其他代码详见 Demo

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

推荐阅读更多精彩内容

  • 8期21天E站到底第8天打卡 用条件格式扮靓报告 一.基本用法: 1.突出显示单元格规则:在开始下方有一个"条件格...
    幽兰_87ab阅读 388评论 0 3
  • 第一部分:通过分列快捷键提取有效信息文本 一、基本用法 分隔符号 选中需要分列的数据-分列→点击下一步-分隔符号(...
    sharon闪闪526阅读 287评论 0 1
  • E战到底第八天! 每天打卡学习已经进入第八天了,即使再忙这个打卡学习已经成了自己生活中的一部分了1 一、条件格式的...
    杨爱鑫阅读 385评论 0 0
  • 要和老公结婚了 老公比我大四岁 上学的时候比我高五个年级 我总问他为什么不早点来找我,当然了这是个无解的问题,我们...
    生气ing_girl阅读 305评论 0 0
  • 是我把“白头偕老”变成了自己的牢,总想着以此作为最后目的,寻寻觅觅,丝毫不让,就算撞到头破血流也在所不惜。现在想来...
    箜莳阅读 149评论 0 3