CoreGraphics绘制图表教程(一)

参考自《Cocoa-Charts》

本篇介绍如何使用CoreGraphics绘制一个坐标系

为了学习如何绘制各种图表,特意去网上找了很多资料,看到了《Cocoa-Charts》这个开源框架,就学习了一下,在这里做一下分解后的记录。

原框架中画了很多类型的图表,包括折线图、柱状图、圆饼图、环形图以及区域填充图等等。我将在本篇开始逐个介绍这些图表是如何从零开始,到完全绘制成功。还有很多其它类型的图表都可以基于这几个基本类型去扩展。

CoreGraphics绘图三板斧

先大体说说CoreGraphics绘图的一些常用的方法,先有个大致了解。后面绘制各类图表基本上就是围绕这几个基本思想去做。

1.获取绘图上下文

CGContextRef context = UIGraphicsGetCurrentContext();

无论你想使用CoreGraphics做什么,你都得先获取到绘图上下文,我们就是依靠它来进行各种绘图操作。

2.画一条线

在绘制区域画一条线,你最少需要用到五个方法。

  • 设置线宽 CGContextSetLineWidth(context, 1);
  • 设置线的起始点 CGContextMoveToPoint(context, 10, 20);
  • 设置线的终点,形成一条绘制路径 CGContextAddLineToPoint(context, 100, 100);
  • 设置画笔的颜色 CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
  • 使用画笔画线 CGContextStrokePath(context);
CGContextRef context = UIGraphicsGetCurrentContext();

CGContextSetLineWidth(context, 1);

CGContextMoveToPoint(context, 10, 20);

CGContextAddLineToPoint(context, 100, 100);

CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);

CGContextStrokePath(context);


3.画一个矩形

CGContextRef context = UIGraphicsGetCurrentContext();
// 设置边框线宽
CGContextSetLineWidth(context, 2);
// 添加一个矩形路径
CGContextAddRect(context, CGRectMake(0, 0, 50, 50));
// 设置画笔颜色
CGContextSetStrokeColorWithColor(context, [UIColor grayColor].CGColor);
// 绘制路径
CGContextStrokePath(context);


4.画一个圆弧或者圆

CGContextRef context = UIGraphicsGetCurrentContext();
// 给定圆周率π
CGFloat pi = 3.141592653f;
// 添加一个圆
CGContextAddArc(context, 100, 100, 50, 0, pi/2, 0);
// 设置画笔颜色
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
// 绘制圆弧
CGContextStrokePath(context);
一段圆弧

解释一下 CGContextAddArc函数的七个参数:

  • 第一个是绘图上下文

  • 第二个是圆心的X坐标

  • 第三个是圆心的Y坐标

  • 第四个是圆的半径

  • 第五个是圆弧的起始弧度

  • 第六个是圆弧的结束弧度

  • 第七个是你是否要指定按照顺时针方向,1为顺时针,0为逆时针

另外我总结了一下各个角度的位置,如图:

我们示例中起始弧度是0度,结束弧度是π/2,顺时针画就是效果图中的1/4圆。

但是细心地同学可能已经看出问题来了,我在 CGContextAddArc函数中设置的第七个参数是 0,对!0代表逆时针。那为什么逆时针会画出1/4圆,而不是3/4圆呢?

我去官方文档找了一下介绍《CGContextAddArc》

The clockwise parameter determines the direction in which the arc is created; the actual direction of the final path is dependent on the current transformation matrix of the graphics context. In a flipped coordinate system (the default for UIView drawing methods in iOS), specifying a clockwise arc results in a counterclockwise arc after the transformation is applied.

翻译:

顺时针参数确定创建弧的方向;最终路径的实际方向取决于图形上下文的当前转换矩阵。在翻转的坐标系中(iOS中UIView绘制方法的默认值),指定顺时针的圆弧会在应用转换后产生逆时针的圆弧。

看明白了吧,默认的就是翻转的,所以你自己没有翻转坐标系的情况下,就理解为 1 是逆时针,0 是顺时针就可以了。

4.1再来画一个,基于上面的例子画一个 1/4圆的扇形

与上面的不同点就是我们要给出起点位置 CGContextMoveToPoint,也就是圆弧的圆心。另外我们要把起点和终点闭合 CGContextClosePath,这样的封闭区域才能画出一个扇形

CGContextRef context = UIGraphicsGetCurrentContext();
// 给定圆周率π
CGFloat pi = 3.141592653f;
// 起始点移动到圆心
CGContextMoveToPoint(context, 100, 100);
// 添加一个圆
CGContextAddArc(context, 100, 100, 50, 0, pi/2, 0);
// 设置画笔颜色
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
// 闭合路径,让起点和终点连接起来
CGContextClosePath(context);
// 绘制圆弧
CGContextStrokePath(context);

效果图:


1/4圆的扇形


5.填充指定区域

就比如我们填充刚刚画的那个 1/4圆的扇形

CGContextRef context = UIGraphicsGetCurrentContext();
// 给定圆周率π
CGFloat pi = 3.141592653f;
// 起始点移动到圆心
CGContextMoveToPoint(context, 100, 100);
// 添加一个圆
CGContextAddArc(context, 100, 100, 50, 0, pi/2, 0);
// 设置填充颜色
CGContextSetFillColorWithColor(context, [UIColor orangeColor].CGColor);
// 填充
CGContextFillPath(context);

效果图:


填充扇形

注意:我这里并没有闭合路径,但是填充依然会成功。因为填充行为会自动为你将起点和终点连接起来(即闭合路径)。但是 StrokePath 画笔画线则不然,它不会自动闭合路径。

我现在给出画1/4圆扇形时错误的例子:

❌错误1:不给出起点也不调用闭合函数就如图 《一段圆弧》所示

❌错误2:画1/4圆扇形的时候,给出起点但不调用闭合函数

CGContextRef context = UIGraphicsGetCurrentContext();
// 给定圆周率π
CGFloat pi = 3.141592653f;
// 起始点移动到圆心
CGContextMoveToPoint(context, 100, 100);
// 添加一个圆
CGContextAddArc(context, 100, 100, 50, 0, pi/2, 0);
// 设置画笔颜色
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
// 不调用闭合函数
// 绘制圆弧
CGContextStrokePath(context);

效果图:


不调用闭合函数画线

❌错误3:填充 1/4 扇形的时候,没有把圆心设为起点

CGContextRef context = UIGraphicsGetCurrentContext();
// 给定圆周率π
CGFloat pi = 3.141592653f;
// 起点未移动到圆心
// 添加一个圆
CGContextAddArc(context, 100, 100, 50, 0, pi/2, 0);
// 设置填充颜色
CGContextSetFillColorWithColor(context, [UIColor orangeColor].CGColor);
// 填充
CGContextFillPath(context);

效果图:


错误原因:之所以只填充了一个“月牙”,还是因为自动闭合路径的缘故。因为没有把圆心设为起点,所以它闭合的是圆弧的起点和终点。就是这段弧:
一段圆弧


6.绘制文字

// 设置字体
UIFont *textFont= [UIFont systemFontOfSize:15];
// 设置属性
NSDictionary *attrs = @{NSFontAttributeName:textFont,
                        NSForegroundColorAttributeName:[UIColor blackColor]};
// 文字的绘制区域
CGRect textRect = CGRectMake(100, 100, 100, 100);
// 绘制文字
[@"Hello World!" drawInRect:textRect withAttributes:attrs];

效果图:



7. 其它要点

  • CGContextAddLineToPoint添加完线段后,起始点也移动到新的端点处,再接 CGContextAddLineToPoint 会从新的端点处开始画

  • CGContextFillPathCGContextStrokePath 会消耗掉当前context中的点和线(即路径),再次调用需要重新设置起始点或路径

  • CGContextFillPath 填充最多可以两条线的路径实现闭合,三条线的路径就失灵了。这条是亲测的!!!

  • CGContextClosePath 在路径的终点和起点之间追加一条线。如果你打算填充一段路径,那么就不需要使用该命令,因为该命令会被自动调用。

  • 两个不相干的封闭路径都可以被一次性正常填充,比如两个矩形




前面这些准备工作做完,就可以去画各种图表了,下面开始进入正题

绘制坐标系

万事开头难,绝大多数图表的绘制都是从坐标系开始的,首先要学会建立一个简单实用的坐标系,为之后绘制各类图表打下一个好基础。

之前介绍过,CoreText的坐标系与我们平时Coding时使用的UIKit坐标系Y轴是倒转的。在这里介绍的CoreGraphics坐标系就不用担心这个问题,我们在绘制过程中用的CoreGraphics坐标系与UIKit完全一致,即原点在屏幕左上角,X轴向右为正方向,Y轴向下为正方向。

绘制坐标系的最终效果图

坐标系的结构组成:

  • 外边框

  • X轴线

  • Y轴线

  • 纬线(横向刻度线,黄色虚线)

  • 经线(纵向刻度线,黄色虚线)

  • X轴刻度(X轴上的标题)

  • Y轴刻度(Y轴上的标题)

  • 十字交叉线(显示触摸点的横纵信息)

总共就上面这几点,这也是绘制一个坐标系的基本思路。

首先,绘制坐标系的外边框

与CoreText一样的是,所有的绘制操作都是从drawRect: 开始,而且同样要获取绘图上下文 CGContextRef

创建一个继承自 UIView 的类,取名 CoordinateSystem,用于绘制坐标系。在 CoordinateSystemdrawRect: 方法中Coding。

Love My Job

好,开始绘制外边框。获取绘图上下文 CGContextRef,并设置绘制区域的填充色,进行填充。

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetFillColorWithColor(context, self.backgroundColor.CGColor);
    CGContextFillRect(context, rect);
}

现在可以去 ViewController 加载一下 CoordinateSystem ,背景颜色设置为cyanColor,效果如下



上图就是填充后的样子,整个绘制区域被填充成 cyanColor

接下来,设置边框线宽-->设置起始点-->在绘制区域添加一个矩形路径-->设置画笔颜色-->绘制路径

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetFillColorWithColor(context, self.backgroundColor.CGColor);
    CGContextFillRect(context, rect);
    
    CGContextSetLineWidth(context, 2);
    
    CGContextMoveToPoint(context, 0.0f, 0.0f);
    CGContextAddRect(context, rect);
    
    CGContextSetStrokeColorWithColor(context, [UIColor grayColor].CGColor);
    CGContextStrokePath(context);
}

效果如下:



绘制X轴线

绘制的X轴线要注意,不能紧贴绘制区域底部,因为后面还要绘制X轴上的刻度,要留出一部分空间。下部间隙暂定 15 像素。

static CGFloat axisMarginBottom = 15;
- (void)drawRect:(CGRect)rect {
    // 获取绘图上下文
    CGContextRef context = UIGraphicsGetCurrentContext();
    // 设置背景填充色
    CGContextSetFillColorWithColor(context, self.backgroundColor.CGColor);
    // 填充整个绘制区域
    CGContextFillRect(context, rect);
    
    // 设置边框线宽
    CGContextSetLineWidth(context, 2);
    // 设置起始点
    CGContextMoveToPoint(context, 0.0f, 0.0f);
    // 添加一个矩形路径
    CGContextAddRect(context, rect);
    // 设置画笔颜色
    CGContextSetStrokeColorWithColor(context, [UIColor grayColor].CGColor);
    // 绘制路径
    CGContextStrokePath(context);
    
    // 重新设置X轴线宽
    CGContextSetLineWidth(context, 1);
    // 设置绘制X轴的起始点
    CGContextMoveToPoint(context, 0.0f, rect.size.height - axisMarginBottom);
    // 添加绘制X轴线的路径
    CGContextAddLineToPoint(context, rect.size.width, rect.size.height - axisMarginBottom);
    // 设置画笔颜色
    CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
    // 绘制路径
    CGContextStrokePath(context);
}

效果如下:



绘制Y轴线

绘制Y轴线要在左侧留出一部分空间,与X轴对称,暂定左侧间距 15 像素

static CGFloat axisMarginBottom = 15;
static CGFloat axisMarginLeft = 15;
- (void)drawRect:(CGRect)rect {
    // 获取绘图上下文
    CGContextRef context = UIGraphicsGetCurrentContext();
    // 设置背景填充色
    CGContextSetFillColorWithColor(context, self.backgroundColor.CGColor);
    // 填充整个绘制区域
    CGContextFillRect(context, rect);
    
    // 设置边框线宽
    CGContextSetLineWidth(context, 2);
    // 设置起始点
    CGContextMoveToPoint(context, 0.0f, 0.0f);
    // 添加一个矩形路径
    CGContextAddRect(context, rect);
    // 设置画笔颜色
    CGContextSetStrokeColorWithColor(context, [UIColor grayColor].CGColor);
    // 绘制路径
    CGContextStrokePath(context);
    
    // 重新设置X轴线宽
    CGContextSetLineWidth(context, 1);
    // 设置绘制X轴的起始点
    CGContextMoveToPoint(context, 0.0f, rect.size.height - axisMarginBottom);
    // 添加绘制X轴线的路径
    CGContextAddLineToPoint(context, rect.size.width, rect.size.height - axisMarginBottom);
    // 设置画笔颜色
    CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
    // 绘制路径
    CGContextStrokePath(context);
    
    // 绘制Y轴线
    CGContextMoveToPoint(context, axisMarginLeft, 0.0f);
    CGContextAddLineToPoint(context, axisMarginLeft, rect.size.height);
    
    CGContextStrokePath(context);
}

效果图:



绘制经线(纵向刻度线、虚线)

纵向的刻度线可以根据数据源的个数,也可以自定固定的个数,我这里是以数据源的个数为准,有多少组数据画多少条经线。

坐标系右侧留出一定空间,暂定右侧间距为 axisMarginRight 3 像素。

- (void)drawLongitudeLines:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, 1);
    CGContextSetStrokeColorWithColor(context, [UIColor orangeColor].CGColor);
    
    if ([self.longitudeTitles count] <= 0) {
        return;
    }
    
    //设置线条为虚线
    CGFloat lengths[] = {3.0, 2.0};
    CGContextSetLineDash(context, 0.0, lengths, 2);
    
    CGFloat postOffset;
    CGFloat offset;
    
    postOffset = (rect.size.width - axisMarginLeft - axisMarginRight) / (self.longitudeTitles.count - 1);
    offset = axisMarginLeft;
    
    for (int i = 1; i < self.longitudeTitles.count ; i++) {
        CGContextMoveToPoint(context, offset + i * postOffset, 0);
        CGContextAddLineToPoint(context, offset + i * postOffset, rect.size.height - axisMarginBottom);
    }
    
    CGContextStrokePath(context);
    CGContextSetLineDash(context, 0, nil, 0);
}

思路:
postOffset 是经线之间的间距,比如要绘制8条经线,那么就把整个绘制区域平均分成7份,每一份就是一个 postOffset

offset 是绘制起始点的X轴坐标,也就是坐标系在左侧留出的空间 axisMarginLeft

这里说明一下设置虚线的参数 lengths,数组第一个参数 3.0 代表线段长度,第二个参数 2.0 代表线段间距。

CGFloat lengths[] = {3.0, 2.0};
CGContextSetLineDash(context, 0.0, lengths, 2);

效果图:



绘制X轴刻度

X轴刻度需要相应的数据源,所以 CoordinateSystem 类要能够接收X轴刻度(X轴标题)数据源,因此添加一个属性 NSArray *longitudeTitles 来接收。

单独写一个方法来绘制X轴刻度:

- (void)drawXAxisTitles:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, 0.5f);
    
    if ([self.longitudeTitles count] <= 0) {
        return;
    }
    
    CGFloat postOffset;
    CGFloat offset;
    
    postOffset = (rect.size.width - axisMarginLeft - axisMarginRight) / (self.longitudeTitles.count - 1);
    offset = axisMarginLeft;
    
    for (int i = 0; i < [self.longitudeTitles count]; i++) {
        
        // 绘制线条
        NSString *valueStr = (NSString *) [self.longitudeTitles objectAtIndex:i];
        UIFont *textFont= [UIFont systemFontOfSize:12]; //设置字体
        NSMutableParagraphStyle *textStyle=[[NSMutableParagraphStyle alloc]init];//段落样式
        textStyle.lineBreakMode = NSLineBreakByWordWrapping;
        
        NSDictionary *attrs = @{NSFontAttributeName:textFont,
                                NSParagraphStyleAttributeName:textStyle,
                                NSForegroundColorAttributeName:[UIColor blackColor]};
        CGSize textSize = [valueStr boundingRectWithSize:CGSizeMake(100, 100)
                                                 options:NSStringDrawingUsesLineFragmentOrigin
                                              attributes:attrs
                                                 context:nil].size;
        
        // 调整X轴坐标位置
        // 第一个刻度的位置要绘制在Y轴线右侧
        if (i == 0) {
            CGRect textRect= CGRectMake(axisMarginLeft, rect.size.height - axisMarginBottom, textSize.width, textSize.height);
            textStyle.alignment=NSTextAlignmentLeft;
            // 绘制字体
            [valueStr drawInRect:textRect withAttributes:attrs];
            
        }
        // 最后一个刻度的位置要绘制在最后一条经线的左侧
        else if (i == self.longitudeTitles.count-1) {
            CGRect textRect= CGRectMake(rect.size.width - axisMarginRight - textSize.width, rect.size.height - axisMarginBottom, textSize.width, textSize.height);
            textStyle.alignment=NSTextAlignmentRight;
            // 绘制字体
            [valueStr drawInRect:textRect withAttributes:attrs];
        } else {
            CGRect textRect= CGRectMake(offset + (i-0.5) * postOffset, rect.size.height - axisMarginBottom, postOffset, textSize.height);
            textStyle.alignment=NSTextAlignmentCenter;
            // 绘制字体
            [valueStr drawInRect:textRect withAttributes:attrs];
        }
    }
}

思路:
把你要绘制在X轴上的刻度数据传进 longitudeTitles 数组中,逐个设置属性,计算 Size,确定每一个刻度的位置,最后使用字符串调用 drawInRect: withAttributes: 方法绘制字体。

效果图:



绘制纬线(横向刻度线、虚线)

与绘制经线一样,我们以数据源的个数为准,Y轴绘制纬线的数据源定为 NSArray *latitudeTitles

我在绘制纬线的时候在上部也同样留出了一定的空间,不让它顶格,好看一点,间距是 axisMarginTop 3 像素

- (void)drawLatitudeLines:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, 1);
    CGContextSetStrokeColorWithColor(context, [UIColor orangeColor].CGColor);
    
    if ([self.latitudeTitles count] <= 0){
        return ;
    }
    //设置线条为虚线
    CGFloat lengths[] = {3.0, 3.0}; // 线宽和间距,长短相间可以画两条虚线,然后拼接在一起
    CGContextSetLineDash(context, 0.0, lengths, 2);
    
    CGFloat postOffset; // 纬线之间的间距
    postOffset = (rect.size.height - axisMarginBottom - axisMarginTop) * 1.0 / ([self.latitudeTitles count] - 1);
    
    CGFloat offset = rect.size.height - axisMarginBottom;
    
    for (int i = 1; i < [self.latitudeTitles count]; i++) {
        CGContextMoveToPoint(context, 0, offset - i * postOffset);
        CGContextAddLineToPoint(context, rect.size.width , offset - i * postOffset);
    }
    CGContextStrokePath(context);
    //还原线条
    CGContextSetLineDash(context, 0, nil, 0);
}

效果图:



绘制Y轴刻度

- (void)drawYAxisTitles:(CGRect)rect {
    if ([self.latitudeTitles count] <= 0) {
        return;
    }
    
    CGFloat postOffset;
    postOffset = (rect.size.height - axisMarginBottom - axisMarginTop) * 1.0 / ([self.latitudeTitles count] - 1);
    
    CGFloat offset = rect.size.height - axisMarginBottom;
    
    for (int i = 0; i < [self.latitudeTitles count]; i++) {
        // 左侧
        // 绘制线条
        NSString *valueStr = (NSString *) [self.latitudeTitles objectAtIndex:i];
        UIFont *textFont= [UIFont systemFontOfSize:12]; //设置字体
        NSMutableParagraphStyle *textStyle=[[NSMutableParagraphStyle alloc]init];//段落样式
        textStyle.lineBreakMode = NSLineBreakByWordWrapping;
        
        NSDictionary *attrs = @{NSFontAttributeName:textFont,
                                NSParagraphStyleAttributeName:textStyle,
                                NSForegroundColorAttributeName:[UIColor blackColor]};
        CGSize textSize = [valueStr boundingRectWithSize:CGSizeMake(100, 100)
                                                 options:NSStringDrawingUsesLineFragmentOrigin
                                              attributes:attrs
                                                 context:nil].size;
        /*
         显示左边
         */
        textStyle.alignment=NSTextAlignmentLeft;
        //调整Y轴坐标位置
        if (i == [self.latitudeTitles count] - 1) {
            CGRect textRect= CGRectMake(axisMarginLeft, offset - i * postOffset, textSize.width, textSize.height);
            //绘制字体
            [valueStr drawInRect:textRect withAttributes:attrs];
        } else {
            CGRect textRect= CGRectMake(axisMarginLeft, offset - i * postOffset - textSize.height - 1, textSize.width, textSize.height);
            //绘制字体
            [valueStr drawInRect:textRect withAttributes:attrs];
        }
    }
}

效果图:



最后,绘制十字交叉线

绘制十字交叉线是本篇最复杂的一个部分,其实也没有那么吓人。原理就是在用户点击或者平移的时候,显示出触摸点的XY轴信息即可。用户触摸点的XY坐标我们是可以获取到的,拿到了XY坐标就可以算出XY轴具体要显示什么信息。

用户点击屏幕时,我们可以通过 touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 方法中的 touches 获取触摸点的XY坐标。

用户平移时,我们可以通过 touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 中的 touches 获取触摸点的XY坐标。

下面是源代码:

/**
 绘制十字交叉线
 
 @param rect 绘制区域
 */
- (void)drawCrossLines:(CGRect)rect {
    
    //过滤非显示区域的点
    if (self.singleTouchPoint.x < axisMarginLeft ||
        self.singleTouchPoint.y < axisMarginTop ||
        self.singleTouchPoint.x > rect.size.width - axisMarginRight ||
        self.singleTouchPoint.y > rect.size.height - axisMarginBottom) {
        return;
    }
    
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, 1.0f);
    
    //设置线条为虚线
    CGFloat lengths[] = {2.0, 2.0};
    CGContextSetLineDash(context, 0.0, lengths, 1);
    
    
    // 绘制纵向刻度文字
    NSString *valueStr = [self calcAxisXGraduate:rect];
    if (![valueStr isEqualToString:@""]) {
        CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
        CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
        
        //绘制纵线
        //还原半透明
        CGContextSetAlpha(context, 1);
        // 移动初始点
        CGContextMoveToPoint(context, self.singleTouchPoint.x, 0);
        // 添加line
        CGContextAddLineToPoint(context, self.singleTouchPoint.x, rect.size.height - axisMarginBottom);
        //绘制线条
        CGContextStrokePath(context);
        
        // 绘制字体
        UIFont *textFont= [UIFont systemFontOfSize:12]; //设置字体
        NSMutableParagraphStyle *textStyle=[[NSMutableParagraphStyle alloc]init];//段落样式
        textStyle.lineBreakMode = NSLineBreakByWordWrapping;
        textStyle.alignment=NSTextAlignmentCenter;
        
        NSDictionary *attrs = @{NSFontAttributeName:textFont,
                                NSParagraphStyleAttributeName:textStyle,
                                NSForegroundColorAttributeName:[UIColor whiteColor]};
        CGSize textSize = [valueStr boundingRectWithSize:CGSizeMake(100, 100)
                                                 options:NSStringDrawingUsesLineFragmentOrigin
                                              attributes:attrs
                                                 context:nil].size;
        CGRect boxRect = CGRectMake(self.singleTouchPoint.x - textSize.width / 2.0, 1, textSize.width, textSize.height);
        
        CGContextAddRect(context,boxRect);
        CGContextFillPath(context);
        
        [valueStr drawInRect:boxRect withAttributes:attrs];
    }
    
    // 绘制横向刻度文字
    NSString *valueStr2 = [self calcAxisYGraduate:rect];
    if (![valueStr2 isEqualToString:@""]) {
        CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
        CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
        
        //绘制横线
        //还原半透明
        CGContextSetAlpha(context, 1);
        
        CGContextMoveToPoint(context, 0, self.singleTouchPoint.y);
        CGContextAddLineToPoint(context, rect.size.width, self.singleTouchPoint.y);
        
        //绘制线条
        CGContextStrokePath(context);
        
        // 绘制字体
        UIFont *textFont2= [UIFont systemFontOfSize:12]; //设置字体
        NSMutableParagraphStyle *textStyle2 = [[NSMutableParagraphStyle alloc] init];//段落样式
        textStyle2.lineBreakMode = NSLineBreakByWordWrapping;
        textStyle2.alignment=NSTextAlignmentLeft;
        
        NSDictionary *attrs2 = @{NSFontAttributeName:textFont2,
                                 NSParagraphStyleAttributeName:textStyle2,
                                 NSForegroundColorAttributeName:[UIColor whiteColor]};
        CGSize textSize2 = [valueStr2 boundingRectWithSize:CGSizeMake(100, 100)
                                                   options:NSStringDrawingUsesLineFragmentOrigin
                                                attributes:attrs2
                                                   context:nil].size;
        CGRect boxRect2 = CGRectMake(1, self.singleTouchPoint.y - textSize2.height / 2.0, textSize2.width, textSize2.height);
        
        CGContextAddRect(context,boxRect2);
        CGContextFillPath(context);
        
        [valueStr2 drawInRect:boxRect2 withAttributes:attrs2];
    }
    
    CGContextSetLineDash(context, 0, nil, 0);
}

// 获取十字交叉线的X轴刻度
- (NSString *)calcAxisXGraduate:(CGRect)rect {
    return [NSString stringWithFormat:@"%f", [self touchPointAxisXValue:rect]];
}

// 获取十字交叉线的Y轴刻度
- (NSString *)calcAxisYGraduate:(CGRect)rect {
    return [NSString stringWithFormat:@"%f", [self touchPointAxisYValue:rect]];
}

// 计算触摸点X坐标值占坐标系宽度比例
- (CGFloat)touchPointAxisXValue:(CGRect)rect {
    CGFloat length = rect.size.width - self.axisMarginLeft - self.axisMarginRight;
    CGFloat valueLength = self.singleTouchPoint.x - self.axisMarginLeft ;
    return valueLength / length;
}

// 计算触摸点Y坐标值占坐标系高度比例
- (CGFloat)touchPointAxisYValue:(CGRect)rect {
    CGFloat length = rect.size.height - self.axisMarginBottom - self.axisMarginTop;
    CGFloat valueLength = length - (self.singleTouchPoint.y - self.axisMarginTop);
    
    return valueLength / length;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    NSArray *allTouches = [touches allObjects];
    //处理点击事件
    if ([allTouches count] == 1) {
        //获取选中点
        self.singleTouchPoint = [[allTouches objectAtIndex:0] locationInView:self];
        //重绘
        [self setNeedsDisplay];
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    
    NSArray *allTouches = [touches allObjects];
    //处理点击事件
    if ([allTouches count] == 1) {
        //获取选中点
        self.singleTouchPoint = [[allTouches objectAtIndex:0] locationInView:self];
        //重绘
        [self setNeedsDisplay];
    }
}

效果图:



Github示例源码

链接地址:CoreGraphicsDrawChart

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

推荐阅读更多精彩内容

  • 【Android 自定义View之绘图】 基础图形的绘制 一、Paint与Canvas 绘图需要两个工具,笔和纸。...
    Rtia阅读 11,655评论 5 34
  • 写在前面 ggplot2 是一个功能强大且灵活的R包 ,由Hadley Wickham 编写,其用于生成优雅的图...
    Boer223阅读 28,020评论 0 67
  • 1 夏夜的凌晨,闷热逼仄的二楼,窗外树叶没有一点动静,风不知躲到哪儿去了。 我的梦在汗水里起起伏伏,拌着老公的呼噜...
    心字罗衣阅读 1,130评论 47 42
  • HTTP状态码(HTTP Status Code)是用以表示网页HTTP响应状态的3位数字代码。
    anyurchao阅读 239评论 0 0