UIBezierPath绘制柱状图、折线图和饼状图

最近用UIBezierPath绘制了一些图形,像柱状图、折线图和饼状图之类的图形。先上效果图:

基本原理:
  • 利用UIBezierPath能够创建基于矢量路径的特性来绘制图形的路径,然后将UIBezierPathCAShapeLayer建立关系,让后者在前者提供的路径中进行渲染,最后生成我们所需的各种图形。而且可以给CAShapeLayer添加动画特效。

一、柱状图

实现思路:
  • 对数据源进行分析,获取数据源的最大值,以及最大值和柱状图高度的比例,用于其他数据等比例显示。数据源为:
- (NSArray *)topArray {
    return @[@"342",@"900",@"505",@"1780",@"1450",@"30",@"1000”];
}
- (NSArray *)bottomArray {
    return @[@"1月",@"2月",@"3月",@"4月",@"5月",@"6月",@"7月”];
}

获取数据源最大值以及比例:

//获取数据最大值
float max = [[self.topArray valueForKeyPath:@"@max.intValue"] floatValue];
//获取比例大小
float scale = (K_HEIGHT-K_LABEL_HEIGHT*2)/max;
  • 然后利用UIBezierPath绘制单个柱状的起点和终点的连线:
//柱状图
UIBezierPath * bePath = [UIBezierPath bezierPath];
//起点
[bePath moveToPoint:CGPointMake(K_WIDTH/(count*2)+K_WIDTH/count*i, K_HEIGHT-K_LABEL_HEIGHT)];
//终点
[bePath addLineToPoint:CGPointMake(K_WIDTH/(count*2)+K_WIDTH/count*i, K_HEIGHT-K_LABEL_HEIGHT-[self.topArray[i] floatValue]*scale)];
[bePath stroke];
  • 创建CAShapeLayer(其属性作用在下面分类中有说明),并与UIBezierPath建立关系,即设置path属性:
//添加CAShapeLayer
_shaLayer = [CAShapeLayer layerWithFillColor:[UIColor clearColor].CGColor strokeColor:[UIColor greenColor].CGColor strokeStart:0.0f strokeEnd:1.0f zPosition:1 lineWidth:30.0f path:bePath.CGPath];
 [self.layer addSublayer:_shaLayer];

CAShapeLayer的分类方法为:

@implementation CAShapeLayer (Category)

/** CAShapeLayer
 * @param fillColor   填充颜色
 * @param strokeColor 填充路径的描边轮廓的颜色
 * @param strokeStart 表示路径的起点,在[0,1]的范围内
 * @param strokeEnd   表示路径的终点,在[0,1]的范围内
 * @param zPosition   表示在superlayer中的位置
 * @param lineWidth   填充路径的线宽
 * @param path        表示要呈现形状的路径
 */
+ (CAShapeLayer *)layerWithFillColor:(CGColorRef)fillColor strokeColor:(CGColorRef)strokeColor strokeStart:(CGFloat)strokeStart strokeEnd:(CGFloat)strokeEnd zPosition:(CGFloat)zPosition lineWidth:(CGFloat)lineWidth path:(CGPathRef)path {
    CAShapeLayer * layer = [CAShapeLayer layer];
    layer.fillColor = fillColor;
    layer.strokeColor = strokeColor;
    layer.strokeStart = strokeStart;
    layer.strokeEnd = strokeEnd;
    layer.zPosition = zPosition;
    layer.lineWidth = lineWidth;
    layer.path  = path;
    return layer;
}
@end
  • 为创建的CAShapeLayer添加动画特效:
//动画
- (void)startStroke {
    [_shaLayer addAnimation:self.pathAnimation forKey:@"strokeEndAnimation”];
}

动画方法为:

//动画
- (CABasicAnimation *)pathAnimation {
    CABasicAnimation * pathAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd”];
    pathAnimation.duration = 2.0f;
    pathAnimation.fromValue = @0.0f;//动画开始位置
    pathAnimation.toValue = @1.0f;//动画停止位置
    pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];//添加动画样式
    return pathAnimation;
}
  • 为每个柱状创建上下数据展示label
//上label
[self addSubview:[UILabel labelWithFrame:CGRectMake(K_WIDTH/count*i, K_HEIGHT-60-[self.topArray[i] floatValue]*scale, K_WIDTH/count, K_LABEL_HEIGHT) text:self.topArray[i] textColor:[UIColor redColor] textAlignment:NSTextAlignmentCenter font:15]];
//下label
[self addSubview:[UILabel labelWithFrame:CGRectMake(K_WIDTH/count*i, K_HEIGHT-K_LABEL_HEIGHT, K_WIDTH/count, K_LABEL_HEIGHT) text:self.bottomArray[i] textColor:[UIColor blackColor] textAlignment:NSTextAlignmentCenter font:13]];
  • 最后按数据源个数对以上控件进行循环创建。
最终实现效果为:
柱状图.gif

二、折线图

折线图的实现方法和柱状图的实现方法类似,主要在于折线图需要创建一个坐标体系,并对数据源中每个坐标点进行连线绘制。直接上代码(数据源和柱状图一样):

#import “BrokenView.h”
#import "UILabel+Category.h”

#define K_WIDTH          CGRectGetWidth(self.frame)
#define K_HEIGHT         CGRectGetHeight(self.frame)
#define K_LABEL_HEIGHT   30
#define K_ACROSS_NUM     6 //横线默认条数
@implementation BrokenView {
    CAShapeLayer * _shaLayer;
}

- (instancetype)initWithFrame:(CGRect)frame topArray:(NSArray *)topArray bottoArray:(NSArray *)bottomArray {
    if (self = [super initWithFrame:frame]) {
        self.backgroundColor = [UIColor whiteColor];
        self.topArray = topArray;
        self.bottomArray = bottomArray;
    }
    return self;
}

//动画
- (void)startStroke {
    [_shaLayer addAnimation:self.pathAnimation forKey:@"strokeEndAnimation”];
}

- (void)drawRect:(CGRect)rect {
    NSUInteger count = self.topArray.count;
    if (count<1) return;
    //获取数据最大值
    float max = [[self.topArray valueForKeyPath:@"@max.intValue"] floatValue];//获取比例大小
    float scale = (K_HEIGHT-K_LABEL_HEIGHT*2)/max;//上下两个label高度和
    
    //绘制坐标系
    for (int i=0; i<K_ACROSS_NUM; i++) {
        //横线
        UIBezierPath * across = [UIBezierPath bezierPath];
        [across moveToPoint:CGPointMake(0, (K_HEIGHT-K_LABEL_HEIGHT)/K_ACROSS_NUM*(i+1))];
        [across addLineToPoint:CGPointMake(K_WIDTH, (K_HEIGHT-K_LABEL_HEIGHT)/K_ACROSS_NUM*(i+1))];
        [[UIColor greenColor] set];
        [across stroke];
    }
    
    for (int j=0; j<count; j++) {
        //竖线
        UIBezierPath * vertical = [UIBezierPath bezierPath];
        [vertical moveToPoint:CGPointMake(K_WIDTH/(count*2)+K_WIDTH/count*j, K_HEIGHT-K_LABEL_HEIGHT)];
        [vertical addLineToPoint:CGPointMake(K_WIDTH/(count*2)+K_WIDTH/count*j, 0)];
        [[UIColor greenColor] set];
        [vertical stroke];
        
        //绘制各坐标点
        UIBezierPath * point = [UIBezierPath bezierPathWithArcCenter:CGPointMake(K_WIDTH/(count*2)+K_WIDTH/count*j, K_HEIGHT-K_LABEL_HEIGHT-[self.topArray[j] floatValue]*scale) radius:5.0f startAngle:-M_PI endAngle:M_PI*3 clockwise:YES];
        CAShapeLayer * pointLayer = [CAShapeLayer layerWithFillColor:[UIColor blueColor].CGColor strokeColor:[UIColor clearColor].CGColor strokeStart:0.0f strokeEnd:1.0f zPosition:0 lineWidth:0.0f path:point.CGPath];
        [self.layer addSublayer:pointLayer];
        
        //上label
        [self addSubview:[UILabel labelWithFrame:CGRectMake(K_WIDTH/count*j, K_HEIGHT-60-[self.topArray[j] floatValue]*scale, K_WIDTH/count, K_LABEL_HEIGHT) text:self.topArray[j] textColor:[UIColor redColor] textAlignment:NSTextAlignmentCenter font:15]];
        //下label
        [self addSubview:[UILabel labelWithFrame:CGRectMake(K_WIDTH/count*j, K_HEIGHT-K_LABEL_HEIGHT, K_WIDTH/count, K_LABEL_HEIGHT) text:self.bottomArray[j] textColor:[UIColor blackColor] textAlignment:NSTextAlignmentCenter font:13]];
    }
    //绘制折线
    UIBezierPath * broPath = [UIBezierPath bezierPath];
    [broPath moveToPoint:CGPointMake(K_WIDTH/(count*2), K_HEIGHT-K_LABEL_HEIGHT-[self.topArray[0] floatValue]*scale)];
    for (int j=1; j<count; j++) {
        [broPath addLineToPoint:CGPointMake(K_WIDTH/(count*2)+K_WIDTH/count*j, K_HEIGHT-K_LABEL_HEIGHT-[self.topArray[j] floatValue]*scale)];
    }
    [broPath stroke];
    _shaLayer = [CAShapeLayer layer];
    _shaLayer.lineWidth = 2.0f;
    _shaLayer.fillColor = [UIColor clearColor].CGColor;
    _shaLayer.strokeColor = [UIColor blueColor].CGColor;
    _shaLayer.path = broPath.CGPath;
    [self.layer addSublayer:_shaLayer];
    
    [self startStroke];
}
@end
最终实现效果为:
折线图.gif

三、饼状图

实现思路:
  • 首先设置饼状图中心点以及半径:
//设置饼状图中心点
CGFloat centerX = K_WIDTH * 0.5f;
CGFloat centerY = K_HEIGHT * 0.5f;
CGPoint centerPoint = CGPointMake(centerX, centerY);
//设置半径
CGFloat radius = MIN(centerX, centerY) * 0.5f;//MIN(A,B)为获取两者最小值
  • 设置数据源:
- (NSArray *)pieArray {
    return @[@"70",@"60",@"100",@"50",@"80"];
}
- (NSArray *)colorArray {
    return @[[UIColor redColor],[UIColor purpleColor],[UIColor blueColor],[UIColor orangeColor],[UIColor blackColor]];
}
  • 获取数据源数据总和,用于后面扇形划分比例:
//获取展示数据总和
CGFloat nums = 0.0f;
for (int i=0; i<self.dataArray.count; i++) {
    nums += [self.dataArray[i] floatValue];
}
  • 创建一个背景圆,用于后期添加动画特效:
    //绘制背景圆的路径
    UIBezierPath * backPath = [UIBezierPath bezierPathWithArcCenter:centerPoint
                                                             radius:radius
                                                         startAngle:-M_PI_2
                                                           endAngle:M_PI_2*3
                                                          clockwise:YES];
    _backLayer = [CAShapeLayer layerWithFillColor:[UIColor clearColor].CGColor
                                      strokeColor:[UIColor greenColor].CGColor
                                      strokeStart:0.0f
                                        strokeEnd:1.0f
                                        zPosition:1
                                        lineWidth:radius * 2.0f
                                             path:backPath.CGPath];

UIBezierPath绘制圆形方法+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;中参数如下:center是弧线中心点的坐标; radius是弧线所在圆的半径; startAngle是弧线开始的角度值; endAngle是弧线结束的角度值; clockwise表示是否顺时针画弧线。

  • UIBezierPath绘制各个扇形的路径,和背景圆路径一样:
//绘制各个扇形的路径
    UIBezierPath * subPath = [UIBezierPath bezierPathWithArcCenter:centerPoint
                                                            radius:radius
                                                        startAngle:-M_PI_2
                                                          endAngle:M_PI_2*3
                                                         clockwise:YES];
  • 分别获取饼状图中每个扇形的起点strokeStart和终点strokeEnd,并按数据源的个数循环创建每个扇形的CAShapeLayer,并与subPath相关联,并以此获取扇形的形状。
 //设置各个扇形开始和结束位置
    CGFloat start = 0.0f;
    CGFloat end = 0.0f;
    for (int i=0; i<self.dataArray.count; i++) {
        end = [self.dataArray[i] floatValue]/nums + start;
        CGColorRef strokeColor = (!self.colorArray ||  self.colorArray.count == 0 || i>self.colorArray.count-1) ? [UIColor purpleColor].CGColor : ((UIColor *)self.colorArray[i]).CGColor;
        CAShapeLayer * subLayer = [CAShapeLayer layerWithFillColor:[UIColor clearColor].CGColor
                                                       strokeColor:strokeColor
                                                       strokeStart:start
                                                         strokeEnd:end
                                                         zPosition:2
                                                         lineWidth:radius * 2.0f
                                                              path:subPath.CGPath];
        [self.layer addSublayer:subLayer];
        
        //百分比label
        CGFloat angle = M_PI * (start + end);//扇形角度
        CGFloat labelCenterX = centerX * 0.5f * sinf(angle) + centerX;
        CGFloat labelCenterY = -centerX * 0.5f * cosf(angle) + centerY;
        UILabel * label = [UILabel labelWithFrame:CGRectMake(0, 0, radius * 0.8f, radius * 0.3f) text:[NSString stringWithFormat:@"%@  %ld%%",self.dataArray[i],(NSInteger)((end-start+0.005)*100)] textColor:[UIColor redColor] textAlignment:NSTextAlignmentCenter font:15];
        label.center = CGPointMake(labelCenterX, labelCenterY);
        label.backgroundColor = [UIColor whiteColor];
        label.layer.zPosition = 3;
        [self addSubview:label];
        
        start = end;
    }

其中以每个扇形的中轴线的中点为中心点来创建的label用于显示扇形的比例。中心点坐标是利用三角形的正弦函数和余弦函数来确定的。

  • 最后为背景圆的Layer添加动画:
//动画
- (void)startStroke {
    [_backLayer addAnimation:self.pathAnimation forKey:@"circleAnimation”];
}
最终实现效果为:

饼状图.gif

Demo地址:UIBezierPath绘制柱状图、折线图和饼状图

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

推荐阅读更多精彩内容

  • 大数据(big data),指无法在一定时间范围内用常规软件工具进行捕捉、管理和处理的数据集合,是需要新处理模式才...
    独孤求学阅读 4,185评论 4 9
  • 日升地平穿重雾,乌云万丈层尽染。 朝霞光辉普大地,万籁处处生机展。
    徐一村阅读 242评论 1 4
  • 街上阴冷无人 秋风归还往事 情怀扰乱思绪 一文前尘旧梦 望君谅无章法 萍水相逢 一声...
    冰槐阅读 634评论 11 9