美文网首页Charts图表
使用CAGradientLayer实现颜色渐变折线图

使用CAGradientLayer实现颜色渐变折线图

作者: iCodingBoy | 来源:发表于2019-10-14 16:50 被阅读0次

    CAGradientLayer是CALayer的一个子类,包含在QuartzCore框架中,支持两种或更多颜色平滑渐变,其绘制使用了硬件加速。本章我们介绍下如何使用CAGradientLayer绘制渐变折线图。

    属性说明

    CAGradientLayer共有5个主要属性:

    /* 
    CGColorRef对象数组,定义了每个渐变区间的渐变色 
    */
    @property(nullable, copy) NSArray *colors;
    
    /* 
    NSNumber对象的可选数组定义了[0,1]范围内的每个渐变停止的位置,值必须是单调增加的。
    如果给出一个nil数组,则渐变在[0,1]范围内均匀分布。
    渲染时,颜色在插值之前映射到输出空间。 默认为nil。
    */
    @property(nullable, copy) NSArray<NSNumber *> *locations;
    
    /*
    绘制到图层坐标空间时渐变的起点和终点。
    起点对应于第一个梯度停止,终点对应于最后一个梯度停止。
    两个点都在单位坐标空间中定义,然后在绘制时将其映射到图层的边界矩形。
    边界矩形即[0,0]是左下角图层的一角,[1,1]是右上角。
    默认值分别是[.5,0]和[.5,1]。 
    */
    @property CGPoint startPoint;
    @property CGPoint endPoint;
    
    /*
    渐变绘制类型,可设置`axial' (默认值), `radial', and `conic'.
    */
    @property(copy) CAGradientLayerType type;
    

    基础渐变

    实现颜色渐变非常简单,只需设置colorsstartPointendPoint属性即可。
    startPointendPoint决定了渐变的方向,这两个参数是以单位坐标系进行的定义,所以左上角坐标是{0, 0},右下角坐标是{1, 1}

    下面是实现紫红和淡红对角线渐变代码:

    @interface ViewController ()
    @property (nonatomic, strong) UIView *gradientContentView;
    @property (nonatomic, strong) CAGradientLayer *gradientLayer;
    @end
    
    @implementation ViewController
    
    - (void)makeConstraints
    {
        __weak typeof(self) weakSelf = self;
        _gradientContentView = [[UIView alloc]init];
        [self.view addSubview:_gradientContentView];
        [self.gradientContentView mas_makeConstraints:^(MASConstraintMaker *make) {
            __strong typeof(weakSelf) strongSelf = weakSelf;
            make.centerX.equalTo(strongSelf.view.mas_centerX);
            make.centerY.equalTo(strongSelf.view.mas_centerY);
            make.width.equalTo(@(240));
            make.height.equalTo(@(300));
        }];
    }
    
    - (void)viewDidLayoutSubviews
    {
        [super viewDidLayoutSubviews];
        _gradientLayer.frame = self.gradientContentView.bounds;
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor whiteColor];
        [self makeConstraints];
        
        _gradientLayer = [CAGradientLayer layer];
        // 设置渐变颜色值
        _gradientLayer.colors = @[(__bridge id)[UIColor colorWithHexString:@"#FF4275"].CGColor,
                                  (__bridge id)[UIColor colorWithHexString:@"#FDF4FA"].CGColor];
        // 设置开始位置和结束点
        _gradientLayer.startPoint = CGPointMake(0, 0);
        _gradientLayer.endPoint = CGPointMake(1, 1);
        [self.gradientContentView.layer addSublayer:_gradientLayer];
    }
    
    
    @end
    

    效果如图:

    均匀渐变

    修改渐变位置:

    // 设置渐变分割位置
    _gradientLayer.locations = @[@0.0, @0.35,@1.0];
    

    效果如图:

    非均匀渐变

    如果你愿意,colors属性可以包含很多颜色,所以创建一个彩虹一样的多重渐变也是很简单的。默认情况下,这些颜色在空间上均匀地被渲染,但是我们可以用locations属性来调整空间。locations属性是一个浮点数值的数组(以NSNumber包装)。这些浮点数定义了colors属性中每个不同颜色的位置,同样的,也是以单位坐标系进行标定。0.0代表着渐变的开始,1.0代表着结束。
    locations数组并不是强制要求的,但是如果你给它赋值了就一定要确保locations的数组大小和colors数组大小一定要相同,否则你将会得到一个空白的渐变。

    目标动画效果

    熟悉CAGradientLayer的基础用法后,我们结合其他Layer实现下图动画展示的复杂渐变折线:

    渐变折线图.gif

    如图:Y轴是数值,X轴是日期,每个日期值对应一个小红点记录(红点支持点击),小红点连成折线,折线以下区域根据Y轴值域进行渐变,从透明到淡紫红色,折线图在记录较多时支持左右滑动

    准备JSON数据

    下面是一组服务器的肤质记录数据,包含测肤时间、照片分数、测试记录id。

    {
        avgScore = 83;
        list =     (
                    {
                measureTime = "2019-03-25 17:46:46";
                photoScore = 64;
                testHistoryId = 1594780;
            },
                    {
                measureTime = "2019-01-25 15:01:13";
                photoScore = 89;
                testHistoryId = 1359786;
            },
                    {
                measureTime = "2019-01-24 16:34:30";
                photoScore = 90;
                testHistoryId = 1355844;
            },
                    {
                measureTime = "2019-01-02 15:18:56";
                photoScore = 74;
                testHistoryId = 1271310;
            },
                    {
                measureTime = "2018-12-29 15:00:02";
                photoScore = 80;
                testHistoryId = 1255037;
            },
                    {
                measureTime = "2018-12-28 09:50:38";
                photoScore = 78;
                testHistoryId = 1250126;
            },
                    {
                measureTime = "2018-12-27 16:54:13";
                photoScore = 85;
                testHistoryId = 1248122;
            },
                    {
                measureTime = "2018-12-26 17:53:59";
                photoScore = 84;
                testHistoryId = 1244881;
            },
                    {
                measureTime = "2018-11-07 09:45:26";
                photoScore = 80;
                testHistoryId = 1033831;
            },
                    {
                measureTime = "2018-10-30 16:48:12";
                photoScore = 82;
                testHistoryId = 1000827;
            },
                    {
                measureTime = "2018-10-29 19:42:25";
                photoScore = 76;
                testHistoryId = 996757;
            },
                    {
                measureTime = "2018-08-21 17:49:42";
                photoScore = 88;
                testHistoryId = 690325;
            },
                    {
                measureTime = "2018-08-17 14:38:39";
                photoScore = 90;
                testHistoryId = 672420;
            },
                    {
                measureTime = "2018-08-16 20:03:22";
                photoScore = 89;
                testHistoryId = 669666;
            },
                    {
                measureTime = "2018-08-10 20:47:43";
                photoScore = 90;
                testHistoryId = 642235;
            },
                    {
                measureTime = "2018-07-19 17:27:44";
                photoScore = 88;
                testHistoryId = 537337;
            },
                    {
                measureTime = "2018-07-18 16:43:24";
                photoScore = 89;
                testHistoryId = 533668;
            },
                    {
                measureTime = "2018-07-17 21:03:19";
                photoScore = 92;
                testHistoryId = 530970;
            }
        );
        maxScore = 92;
    }
    

    定义数据模型

    1、定义数据模型

    针对以上JSON数据,定义数据模型MGLDRecordModel如下:

    @interface MGLDRecordModel : NSObject
    @property (nonatomic, strong) NSString *testHistoryId;
    @property (nonatomic, strong) NSNumber *photoScore;
    @property (nonatomic, strong) NSString *measureTime;
    // 按照图表 `月/日` 进行时间格式化
    - (NSString*)formattedTime;
    @end
    
    @implementation MGLDRecordModel
    - (NSString*)formattedTime
    {
        if (self.measureTime) {
            return nil;
        }
        NSDate *date = [NSDate dateWithString:self.measureTime formatString:@"yyyy-MM-dd HH:mm:ss"];
        return [NSString stringWithFormat:@"%@/%@",@(date.month),@(date.day)];
    }
    @end
    

    2、JSON-Model序列化

    提取上述JSONlist数组并进行对象序列化

    NSDictionary *jsonObject = [self getResponseJSON];
    if (jsonObject && [jsonObject.allKeys containsObject:@"list"]) 
    {
        NSArray *list = jsonObject[@"list"];
            
        NSMutableArray *tmpArray = [[NSMutableArray alloc]init];
        for (int index = 0; index < list.count; index++) {
           NSDictionary *param = list[index];
             MGLDRecordModel *aModel = [MGLDRecordModel modelWithJSON:param];
            if (aModel) {
                 [tmpArray addObject:aModel];
             }
        }
            // 绘图
    }
    

    设计图层结构

    从目标动画gif图可以看出,折线图的宽度大于可见区域,不能一次性全部显示,X轴的日期跟随折线图移动,Y轴和背景虚实线保持静止,可拆解主要组成元素为以下三部分:

    • 底部背景视图(MGLDHistoryGraphView类)

    背景图层主要用于显示Y坐标、虚实线,并容纳滚动容器,本例中Y坐标使用UILabel标签布局组成,虚实线采用Draw方法绘制.

    • 中间滚动容器(UIScrollView类)

    中间滚动容器采用UIScrollView,边界为父容器的Y轴右边边界区域,contentSize与画板size保持一直

    • 核心画板(MGLDDrawingBoard类)

    画板用于绘制渐变区域、渐变折线,容纳交互按钮、X轴日期标签等,画板的宽度根据记录的数量动态计算

    视图类自底而上依次为MGLDHistoryGraphView->UIScrollView->MGLDDrawingBoard

    图层结构如下:

    图层结构

    开始绘图

    1、绘制底层背景

    按照上述图层设计,我们从下往上先绘制底层的背景如下:


    底层坐标轴

    如图,最底层视图主要由Y坐标、虚线、实线和白色背景组成,我们依次绘制:

    1.1、绘制Y坐标轴

    - (void)drawYAxisValuesInRect:(CGRect)rect
    {
        CGFloat height = self.bounds.size.height;
        // static CGFloat KTopSpaceValue = 20.0f; static CGFloat KBottomSpaceValue = 34.0f;
        CGFloat drawContainerHeight = height - KTopSpaceValue - KBottomSpaceValue;
        CGFloat dashLinePadding = drawContainerHeight / 5.0;
        
        NSArray *yAxisValue = @[@"100",@"80",@"60",@"40",@"20"];
        NSUInteger count = yAxisValue.count;
        for (int index = 0; index < count; index++)
        {
            CGFloat originY = KTopSpaceValue + index*dashLinePadding - 9;
            UILabel *label = [[UILabel alloc]initWithFrame:CGRectMake(0, originY, KYAxisLeftSpaceValue-5, 18)];
            label.textAlignment = NSTextAlignmentRight;
            label.backgroundColor = [UIColor clearColor];
            label.font = [UIFont systemFontOfSize:12];
            label.textColor = [UIColor colorWithWhite:0.5  alpha:1.0];
            label.text = yAxisValue[index];
            [self addSubview:label];
        }
    }
    

    1.2、绘制辅助虚线

    - (void)drawAuxiliaryDashedLineInRect:(CGRect)rect
    {
        CGFloat height = self.bounds.size.height;
        CGFloat width = self.bounds.size.width;
        CGFloat drawContainerHeight = height - KTopSpaceValue - KBottomSpaceValue;
        CGFloat dashLinePadding = drawContainerHeight / 5.0;
        
        [[UIColor colorWithHexString:@"#E0E1E7"]setStroke];
        UIBezierPath *path = [UIBezierPath bezierPath];
        path.lineWidth = 1.0;
        CGFloat dash[] = {6,5};
        [path setLineDash:dash count:2 phase:0];
        for (int index = 0; index < 5; index ++) {
            [path moveToPoint:CGPointMake(KYAxisLeftSpaceValue, KTopSpaceValue + index*dashLinePadding)];
            [path addLineToPoint:CGPointMake(width, KTopSpaceValue + index*dashLinePadding)];
        }
        [path stroke];
    }
    
    

    1.3、绘制底部实线

    - (void)drawBottomLineInRect:(CGRect)rect
    {
        CGFloat height = self.bounds.size.height;
        CGFloat width = self.bounds.size.width;
        
        UIBezierPath *path = [UIBezierPath bezierPath];
        
        [path moveToPoint:CGPointMake(KYAxisLeftSpaceValue, height - KBottomSpaceValue)];
        [path addLineToPoint:CGPointMake(width, height - KBottomSpaceValue)];
        
        [[UIColor colorWithHexString:@"#E0E1E7"]setStroke];
        path.lineWidth = 1.0;
        [path stroke];
    }
    

    2、添加UIScrollView

    添加UIScrollView,保持top、bottom、right与父视图一致,左边从Y坐标轴右边区域开始,背景透明,为了便于观察,我们添加#FF4275背景色

    添加UIScrollView

    布局如下:

    @weakify(self);
    _scrollView = [[UIScrollView alloc]init];
    // 设置紫红背景色便于观察
    _scrollView.backgroundColor = [UIColor colorWithHexString:@"#FF4275"];
    _scrollView.showsVerticalScrollIndicator = NO;
    _scrollView.showsHorizontalScrollIndicator = NO;
    [self addSubview:_scrollView];
    [self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
        @strongify(self);
        make.left.equalTo(self).offset(KYAxisLeftSpaceValue);
        make.right.top.bottom.equalTo(self);
    }];
    

    3、添加画板

    画板是整个折线图的核心部分,用来绘制渐变色,X轴坐标、折线、交互按钮。

    添加MGLDDrawingBoard画板到UIScrollView,设置边缘与UIScrollView保持一直,高度等于UIScrollView高度,宽度根据折线图绘制的宽度计算。

    _drawingBoard = [[MGLDDrawingBoard alloc]init];
    // 设置橙色背景色便于观察
    _drawingBoard.backgroundColor = [UIColor orangeColor];   
    [self.scrollView addSubview:_drawingBoard];
    [self.drawingBoard mas_makeConstraints:^(MASConstraintMaker *make) {
        @strongify(self);
        make.edges.equalTo(self.scrollView);
        make.height.equalTo(self.scrollView.mas_height);
        // 初始默认宽度,绘制曲线时需要更新
        make.width.equalTo(@(10));
    }];
    

    效果如图:

    画板布局图

    3.1、背景图层接口配置

    背景图层接口调用进行绘制更新时,需要根据记录个数调整画板的宽度,记录过少时在画板绘制区间均匀分布,记录过多时固定宽度分布,左右滚动查看

    /**
     折线图容器,包含UIScrollView、画板以及其他元素
     */
    @interface MGLDHistoryGraphView : UIView
    - (void)drawGraphWithValues:(NSArray<MGLDRecordModel*>*)values;
    @end
    
    @implementation MGLDHistoryGraphView
    - (void)drawGraphWithValues:(NSArray<MGLDRecordModel *> *)values
    {
        NSUInteger count = values.count;
        self.itemCount = count;
        [self setNeedsLayout];
        // 绘制渐变折线图
        [self.drawingBoard drawGraphWithValues:values];
    }
    
    /*
     折线图在记录过少需要在可见屏幕均匀分布,记录过多左右滚动
     我们需要在此函数调用时刷新画板的边界
    */
    - (void)layoutSubviews
    {
        [super layoutSubviews];
        CGFloat width = CGRectGetWidth(self.bounds);
        // 如果可绘制的记录总宽度小于当前容器的宽度,则让记录在容器空间均匀分布
        if (self.itemCount*KItemSpaceValue < width)
        {
            [self.drawingBoard mas_updateConstraints:^(MASConstraintMaker *make) {
                make.width.equalTo(@(width));
            }];
        }
        else
        {
            CGFloat drawingBoardWidth = (self.itemCount+2)*KItemSpaceValue;
            [self.drawingBoard mas_updateConstraints:^(MASConstraintMaker *make) {
                make.width.equalTo(@(drawingBoardWidth));
            }];
        }
    }
    @end
    

    3.2、画板接口配置

    在画板页面,我们将交互按钮和X轴坐标分别用UIButton和UILabel进行展示,这里预先缓存控件用于后面的layout刷新。

    /**
     画板,用于折线图内容绘制
     */
    @interface MGLDDrawingBoard : UIView
    - (void)drawGraphWithValues:(NSArray<MGLDRecordModel*>*)values;
    @end
    
    - (void)drawGraphWithValues:(NSArray<MGLDRecordModel *> *)values
    {
    // 加锁,防止重新绘制和layout布局同时调用造成资源访问冲突
        [_lock lock];
        self.drawValues = values;
        if (values.count > 0) {
             // 默认选中最后一个
            MGLDRecordModel *aModel = [values lastObject];
            self.selectedRecordId = aModel.testHistoryId;
        }
        // 移除按钮和标签
        [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
        
        // 缓存画板上的按钮和标签
        NSMutableArray *buttonsArray = [NSMutableArray array];
        NSMutableArray *labelsArray = [NSMutableArray array];
        for (int index = 0; index < values.count; index++) {
            MGLDRecordModel *aModel = [values objectAtIndex:index];
            UIButton *button = [self buttonWithRecordId:aModel.testHistoryId tag:index];
            [self addSubview:button];
            [buttonsArray addObject:button];
            
            UILabel *label = [self labelWithText:[aModel formattedTime]];
            [self addSubview:label];
            [labelsArray addObject:label];
        }
        self.buttons = buttonsArray;
        self.labels = labelsArray;
        [_lock unlock];
        // 触发layout布局
        [self setNeedsLayout];
    }
    

    3.3、设置画板渐变和折线图层

    @interface MGLDDrawingBoard ()
    @property (nonatomic, strong) CAGradientLayer *gradientLayer;
    @property (nonatomic, strong) CAShapeLayer *lineLayer;
    @property (nonatomic, strong) NSArray<MGLDRecordModel *> *drawValues;
    @property (nonatomic, strong) NSArray *buttons;
    @property (nonatomic, strong) NSArray *labels;
    @property (nonatomic, strong) NSLock *lock;
    @property (nonatomic, strong) NSNumber *selectedRecordId;
    @end
    
    @implementation MGLDDrawingBoard
    - (void)setUp
    {
        self.backgroundColor = [UIColor clearColor];
        _lock = [[NSLock alloc]init];
        // 渐变图层
        _gradientLayer = [CAGradientLayer layer];
        NSArray *colors = @[(__bridge id)[UIColor colorWithHexString:@"#FF4275"].CGColor,
                            (__bridge id)[UIColor colorWithHexString:@"#FFC7D6"].CGColor,
                            (__bridge id)[UIColor colorWithHexString:@"#FFE4EC"].CGColor,
                            (__bridge id)[UIColor colorWithHexString:@"#FFF5F7"].CGColor,
                            (__bridge id)[UIColor whiteColor].CGColor];
        _gradientLayer.colors = colors;
        _gradientLayer.locations = @[@0.0,@0.4,@0.6,@0.8,@1.0];
        _gradientLayer.startPoint = CGPointMake(0.5,0.0);
        _gradientLayer.endPoint = CGPointMake(0.5,1.0);
        _gradientLayer.opacity = 0.5;
        [self.layer addSublayer:_gradientLayer];
        
        // 折线图层
        _lineLayer = [CAShapeLayer layer];
        _lineLayer.allowsEdgeAntialiasing = YES;
        _lineLayer.strokeColor = [UIColor colorWithHexString:@"#FF4275"].CGColor;
        _lineLayer.fillColor = [UIColor clearColor].CGColor;
        _lineLayer.lineWidth = 2.0;
        _lineLayer.lineJoin = kCALineJoinRound;
        _lineLayer.lineCap = kCALineCapRound;
        [self.layer addSublayer:_lineLayer];
    }
    @end
    

    3.4、图层绘制和按钮标签布局

    这里将layer路径设置和按钮、标签坐标更新放到一起计算

    - (void)layoutSubviews
    {
        [super layoutSubviews];
        // 调整layer的框架
        _lineLayer.frame = self.bounds;
        _gradientLayer.frame = self.bounds;
        
        [_lock lock];
        if (self.drawValues.count <= 0) {
            [_lock unlock];
            return;
        }
        
        CGFloat width = CGRectGetWidth(self.bounds);
        CGFloat height = CGRectGetHeight(self.bounds);
        CGFloat drawSpaceHeight = height - KTopSpaceValue - KBottomSpaceValue;
        
        
        MGLDRecordModel *firstModel = [self.drawValues firstObject];
        MGLDRecordModel *lastModel = [self.drawValues lastObject];
        
        CGFloat firstPointY = [self getOriginY:firstModel.photoScore.floatValue spaceHeight:drawSpaceHeight];
        CGFloat lastPointY = [self getOriginY:lastModel.photoScore.floatValue spaceHeight:drawSpaceHeight];
        
        UIBezierPath *gradientPath = [UIBezierPath bezierPath];
        UIBezierPath *linePath = [UIBezierPath bezierPath];
        [gradientPath moveToPoint:CGPointMake(-1, firstPointY)];
        [linePath moveToPoint:CGPointMake(0, firstPointY)];
        
        NSUInteger count = self.drawValues.count;
        CGFloat superWidth =  CGRectGetWidth(self.superview.frame);
        if (count*KItemSpaceValue < superWidth)
        {
            CGFloat padding = width/(count + 1);
            CGFloat originX = (width - MAX(0, count-1)*padding)/2;
            for (int index = 0; index < count; index++)
            {
                MGLDRecordModel *aModel = [self.drawValues objectAtIndex:index];
                CGFloat pointY = [self getOriginY:aModel.photoScore.floatValue spaceHeight:drawSpaceHeight];
                // 添加路径
                CGPoint pathPoint = CGPointMake(index*padding + originX, pointY);
                [gradientPath addLineToPoint:pathPoint];
                [linePath addLineToPoint:pathPoint];
                
                // 更新按钮和标签的center
                if (index < self.buttons.count) {
                    UIButton *button = [self.buttons objectAtIndex:index];
                    button.center = pathPoint;
                }
                CGPoint labelPoint = CGPointMake(index * padding + originX, height - KBottomSpaceValue*0.5);
                if (index < self.labels.count) {
                    UILabel *label = [self.labels objectAtIndex:index];
                    label.center = labelPoint;
                }
            }
        }
        else
        {
            for (int index = 0; index < count; index++)
            {
                MGLDRecordModel *aModel = [self.drawValues objectAtIndex:index];
                CGFloat pointY = [self getOriginY:aModel.photoScore.floatValue spaceHeight:drawSpaceHeight];
                CGPoint pathPoint = CGPointMake((index + 1)*KItemSpaceValue, pointY);
                [gradientPath addLineToPoint:pathPoint];
                [linePath addLineToPoint:pathPoint];
                if (index < self.buttons.count) {
                    UIButton *button = [self.buttons objectAtIndex:index];
                    button.center = pathPoint;
                }
                CGPoint labelPoint = CGPointMake((index + 1)*KItemSpaceValue, height - KBottomSpaceValue*0.5);
                if (index < self.labels.count) {
                    UILabel *label = [self.labels objectAtIndex:index];
                    [label sizeToFit];
                    label.center = labelPoint;
                }
            }
        }
        
        [gradientPath addLineToPoint:CGPointMake(width+1, lastPointY)];
        [linePath addLineToPoint:CGPointMake(width, lastPointY)];
        
        // 构造渐变layer闭环
        [gradientPath addLineToPoint:CGPointMake(width+1, height - KBottomSpaceValue)];
        [gradientPath addLineToPoint:CGPointMake(-1, height - KBottomSpaceValue)];
        [gradientPath addLineToPoint:CGPointMake(-1, firstPointY)];
        
        CAShapeLayer *shapeLayer = [CAShapeLayer layer];
        shapeLayer.path = gradientPath.CGPath;
        _gradientLayer.mask = shapeLayer;
        _lineLayer.path = linePath.CGPath;
        
        // 设置选中状态,第一次排版时刷新布局并滚动到对应位置
        for (UIButton *button in self.buttons) {
            if (self.selectedRecordId && [button.recordId isEqualToNumber:self.selectedRecordId]) {
                button.selected = YES;
                CGFloat originX = button.frame.origin.x;
                UIScrollView *scrollView = (UIScrollView*)self.superview;
                if (scrollView.frame.size.width < scrollView.contentSize.width) {
                    CGFloat width = scrollView.frame.size.width;
                    CGFloat offsetX = MAX(0, originX + KItemSpaceValue-width);
                    [(UIScrollView*)self.superview setContentOffset:CGPointMake(offsetX, 0) animated:YES];
                }
            }
            else
            {
                button.selected = NO;
            }
        }
        
        [_lock unlock];
    }
    

    最终完成效果同上:


    渐变折线图.gif

    注:实际上画板上的渐变、折线、按钮和标签都是相互独立的部分,经过组合得到图示效果。

    源代码

    附上Demo源代码

    相关文章

      网友评论

        本文标题:使用CAGradientLayer实现颜色渐变折线图

        本文链接:https://www.haomeiwen.com/subject/dxjzectx.html