swift:打造你自己的折线图

看到苹果Health里的折线图了吗。我们就是要打造一个这样的折线图。没看过的请看下图。

我们的主题在于折线图本身。其他的包括步数、日平均值等描述类的内容这里就不涉及了。

首先观察,这个图种包含些什么组成部分。线?这个太明显都看见了。还有每个节点的小圆圈,还有折线图里从上到下的渐变。这里是白色的从上到下逐渐透明的效果。还有一条虚线。这个暂时先不考虑了。你能绘制出来最下面的x轴标尺,绘制个虚线还不是小菜?

为什么说是绘制呢,因为显然我们不想用一个UIView把像素设置为1,背景色设置为UIColor.whiteColor(),然后设置View 的倾斜度的方式来堆砌这个line chat。首先必须严重的鄙视这种做法。在开发中不能光是把各种UIButton、UILabel什么的设定好了frame就网上没完没了的堆。或者更有 甚者直接拖动这些控件到Storyboard上。摆个位置,设置个宽和高别的就完全不管了。autolayout什么的一概不问,使用了 storyboard也适配不了多分辨率。这样的结果是谁维护代码谁遭殃。

正确的做法是提升代码。有多个地方都用到同样的组合控件的时候,比如多选框、单选框,就自定义一个。这样,至少可以达到一改全改的效果。代码维护简 单了很多。同时需要考虑效率的问题。比如我们的line chart,就使用Core Graphics和QuartzCore框架中的CAShapeLayer绘制。这样执行效率明显比堆砌UIView的方法效率高--占用资源少,执行 快。

看看CALayer的定义:

class CALayer : NSObject, NSCoding, CAMediaTiming

再看看UIView的定义:

class UIView : UIResponder, NSCoding, UIAppearance, NSObjectProtocol, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace

你就应该知道为什么完全不能用UIView来堆砌这个图了。

言归正传!画线可以用Core Graphics一点点的画,也可以用CALayer来话,准确的说是CAShapeLayer更方便,所以我们用CAShapeLayer来画线。用 CAShapeLayer画线灰常之简单。总的来说就是设定路线(Path),然后把这个路线赋值给这个layer画线就完成了。比如,初始化一条贝塞尔 曲线,然后指定好center point和半径,起始角度和结束角度,然后“BANG”。“BANG”是一个象声词,龙珠里很多。指定你的CAShapeLayer实例的path属性 值为这个path。此处略去一堆什么给你的view.layer.addsublayer什么的细节。运行后你就会看到一个从起始角度到结束角度的一个半 圆。

运行起来之后,你会看到这个半圆和你需要的起始角度、结束角度差很多。所以,还是画一个正圆比较容易一些。尤其现在我们才刚刚开始接触这个神秘的东 东。等下还有更神秘的。。。要画正圆只要指定起始角度为0(这里需要严重说明一下,角度都是弧度制的,比如,π、2π什么的)。结束角度为2π,也就是(M_PI * 2)。半径随便,圆心最好设定在屏幕的中心,也就是:

UIScreen.mainScreen().bounds.height / 2和UIScreen.mainScreen().bounds.width / 2。这样就是在屏幕中心点,以你给定的值为半径画了一个圆圈。效果如图:

给的贝塞尔曲线是这样的:

UIBezierPath(arcCenter: centerPoint, radius: CGRectGetWidth(bounds) / 2 - 30.0, startAngle: 0, endAngle: CGFloat(M_PI * 2.0), clockwise: true).CGPath

这里需要注意的是一定要在最后调用属性CGPath,这个才是CAShapeLayer可以接受的Path的类型。直接赋值是会报错的。在贝塞尔曲 线初始化的过程中角度值需要使用CGFloat类型。M_PI是Double类型的。这里需要类型转换一下。否则报错会报在radius的身上,但是起始 是角度的类型问题。

圆是画出来了,但是我们要绘制的是line chart,是直线。该如何解决呢。这里就需要说明一下绘制线的一般感性认识。首先CAShapeLayer需要知道绘制的起始点在哪里,其次,从哪一点 到哪一点绘制一条线。对于圆的贝塞尔曲线来说自然是从角度为0的,半径长度和圆心来开始画线,线一直延续到结束角度2π(PI)。对于一条直线就简单多 了。起点是指定的一个点。然后,添加一条线到另一个点。来看看如何具体的用代码画一条线。

        var path = CGPathCreateMutable()
        var x = UIScreen.mainScreen().bounds.width / 2, y = UIScreen.mainScreen().bounds.height / 5
        CGPathMoveToPoint(path, nil, 0, y * 2)

CGPathAddLineToPoint(path, nil, 0, 0)

CGPathAddLineToPoint(path, nil, x - kRadiusLength, 0)

        CGPathAddLineToPoint(path, nil, bounds.size.width, bounds.size.height)

        progressLayer.path = path    

线就是这么画出来的。有线了以后就需要考虑另一个问题了,线下面的渐变色。这个就需要用到另一种Layer:CAGradientLayer。CAGradientLayer有一个属性可以做到这一点,这个属性就是colors。给这个属性多少颜色,CAGradientLayer就会出现多少从一个颜色到另一个颜色的渐变。注意一点,这里需要的颜色都是UIColor.yellowColor().CGColor。看到这个CGColor了吗?一定要这个颜色才行。否则,不报错,也不显示任何的颜色!

代码:

        var gradientLayer2 = CAGradientLayer()
        gradientLayer2.startPoint = CGPointMake(0.5, 1.0)
        gradientLayer2.endPoint = CGPointMake(0.5, 0.0)
        gradientLayer2.frame = CGRectMake(0, 0, bounds.size.width, bounds.size.height)
        gradientLayer2.colors = [UIColor.yellowColor().CGColor, UIColor.blueColor().CGColor, UIColor.greenColor().CGColor]
        self.view.layer.addSublayer(gradientLayer2)

这效果就出来了:

到这里你应该就明白了。图一种的白色到透明的渐变其实就是不同alpha的白色赋值给了colors属性。

gradientLayer2.colors = [UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.0).CGColor,
            UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.5).CGColor,
            UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.8).CGColor]

看效果,白色从上到下的渐变填充已经出来了。画线前面已经讲过。现在的问题就是让这个填充按照画得线剪裁。这个非常简单。

我们来给上面的CAShapeLayer这样的一个路线:

var path = CGPathCreateMutable()
        CGPathMoveToPoint(path, nil, 0, UIScreen.mainScreen().bounds.height)
        CGPathAddLineToPoint(path, nil, 0, 0)
        CGPathAddLineToPoint(path, nil, x - kRadiusLength, 0)
        CGPathAddLineToPoint(path, nil, bounds.size.width, bounds.size.height / 2)

然后,就让CAGradientLayer的mask属性为这个CAShapeLayer。

gradientLayer.mask = progressLayer

这样一来。效果就出来了。

但是。。仔细一个,填充的渐变白色图是有了,那么线呢?白色的线没有。CAShapeLayer的线最终都只是成为CAGradientLayer的剪裁线。要解决这个问题就要上下面的重头戏了。

为了解决这个问题,我们不得不祭出Core Graphics神器了。总体的构造思路是在Controller中添加一个View,在这个View中使用Core Graphics来画线,之后在上面添加我们上文说到的两个Layer。也就是下面画线,然后用Layer来完成渐变色的填充和对这个填充色的剪裁。

Core Graphics画线比CALayer还是麻烦一些的,但是思路总体上一致。也是把画笔放到起始点(在哪里开始画线)。之后也是从哪里到哪里画线。总体来说,画线的思路就是这样。

首先,需要在Core Graphics中铺上画布:

var context = UIGraphicsGetCurrentContext()

2. 指定线的颜色和线的宽度:

CGContextSetStrokeColorWithColor(context, UIColor.whiteColor().CGColor)
CGContextSetLineWidth(context, 1.0)

3. 开始画线:

CGContextMoveToPoint(context, kBottomMargin, CGRectGetHeight(rect) - kBottomMargin)
CGContextAddLineToPoint(context, CGRectGetWidth(rect) - kBottomMargin, CGRectGetHeight(rect) - kBottomMargin)

这里必须补充一点。在画线的时候,我们需要一些列的点坐标。暂时,只是用模拟的方式实现。var x = calculateX(0)var y = calculateY(0)就是第一个点得x,y坐标的计算方法。具体的代码在后面。这些给定的点需要映射到你的画布的坐标系中。calculateX、Y就是做这个映射的。虽然省略了一些步骤。但是你应该可以从初中的数学基础中明白这个是怎么回事的,所以此处只做解释其他省略。

    func calculateX(i: Int) -> CGFloat {
        var x = kBottomMargin + CGFloat(i) * kUnitLabelWidth!
        return x
    }

kBottomMargin是x点在左侧的一个margin。只是展示需要,不用关心。 CGFloat(i) * kUnitLabelWidth!,i是第几个点,也就是x轴上的index。kUnitLabelWidth!是x轴上两点之间的距离,至于感叹号就不多解释了,那个是swift的基础。

func calculateY(i: Int) -> CGFloat {
        var y: CGFloat = 0
        switch(i){
        case 0:
            y = kTotalYValue! * 0.5
            break
        case 1:
            y = kTotalYValue! * 0.3
            break
        case 2:
            y = kTotalYValue! * 0.7
            break
        case 3:
            y = kTotalYValue! * 0.7
            break
        case 4:
            y = kTotalYValue! * 0.2
            break
        case 5:
            y = kTotalYValue! * 0.8
            break
        default:
            y = 0
            break
        }
        return y
    }

这里主要计算,每个x点对应的y点(这里就摸你了y值对应在画布坐标系的方法)。

有了以上的只是就可以画出折线图了。具体的方法如下:

override func drawRect(rect: CGRect) {
        println("drawRect")

        var context = UIGraphicsGetCurrentContext()

//        CGContextSetStrokeColorWithColor(context, UIColor.blueColor().CGColor)
//        CGContextSetLineWidth(context, 4.0)
//        CGContextMoveToPoint(context, kBottomMargin, kBottomMargin)
//        CGContextAddLineToPoint(context, CGRectGetWidth(rect) - kBottomMargin, CGRectGetHeight(rect) - kBottomMargin)
//        CGContextStrokePath(context)

        CGContextSetStrokeColorWithColor(context, UIColor.whiteColor().CGColor)
        CGContextSetLineWidth(context, 1.0)
        CGContextMoveToPoint(context, kBottomMargin, CGRectGetHeight(rect) - kBottomMargin)
        CGContextAddLineToPoint(context, CGRectGetWidth(rect) - kBottomMargin, CGRectGetHeight(rect) - kBottomMargin)
//        CGContextStrokePath(context)

        CGContextSetFillColorWithColor(context, UIColor.orangeColor().CGColor)

        var x = calculateX(0)
        var y = calculateY(0)
        var prePoint: CGPoint = CGPointMake(x, y)

        for var index = 0; index < 6; index++ {
            var x = calculateX(index)
            var y = calculateY(index)
            var textY = CGRectGetHeight(rect) - kBottomMargin + 3

            CGContextMoveToPoint(context, x, CGRectGetHeight(rect) - kBottomMargin)
            CGContextAddLineToPoint(context, x, CGRectGetHeight(rect) - kBottomMargin + kUnitLabelHeight)

            var labelString = NSString(string: "\(kBaseLabelString) \(index)")
            labelString.drawAtPoint(CGPointMake(x + kUnitLabelHeight, textY), withAttributes: [NSFontAttributeName: kLabelFont, NSForegroundColorAttributeName: kLabelFontColor])

            CGContextStrokePath(context)

            CGContextMoveToPoint(context, x, y)
//            CGContextSetLineWidth(context, 2.0)
            var path = UIBezierPath(arcCenter: CGPointMake(x, y), radius: kCircleRadiusLength, startAngle: CGFloat(0.0)
                , endAngle: CGFloat(2 * M_PI), clockwise: true)
            CGContextAddPath(context, path.CGPath)
//            CGContextFillPath(context)

            CGContextStrokePath(context)

//            var offset: CGFloat = kCircleRadiusLength * CGFloat(sin(M_PI_4))
            var offset = calculateOffset(prePoint.x, prePoint.y, x, y, kCircleRadiusLength)
            if prePoint.x != x /*&& prePoint.y != y*/ {

                if y > prePoint.y {
                    CGContextMoveToPoint(context, prePoint.x + offset.offsetX, prePoint.y + offset.offsetY)
                    CGContextAddLineToPoint(context, x - offset.offsetX, y - offset.offsetY)
                }
                else if y < prePoint.y {
                    CGContextMoveToPoint(context, prePoint.x + offset.offsetX, prePoint.y - offset.offsetY)
                    CGContextAddLineToPoint(context, x - offset.offsetX, y + offset.offsetY)
                }
                else{
                    CGContextMoveToPoint(context, prePoint.x + offset.offsetX, prePoint.y)
                    CGContextAddLineToPoint(context, x - offset.offsetX, y)
                }

                CGContextStrokePath(context)

                prePoint = CGPointMake(x, y)
            }
        }

//        CGContextMoveToPoint(context, x, y)
        CGContextSetLineWidth(context, 3)
        CGContextSetStrokeColorWithColor(context, UIColor.greenColor().CGColor)
        CGContextSetFillColorWithColor(context, UIColor.blueColor().CGColor)
        var circleRect = CGRectMake(x, y, 15, 15)
        circleRect = CGRectInset(circleRect, 3, 3)
        CGContextFillEllipseInRect(context, circleRect)
        CGContextStrokeEllipseInRect(context, circleRect)
    }

这一段代码:

var path = UIBezierPath(arcCenter: CGPointMake(x, y), radius: kCircleRadiusLength, startAngle: CGFloat(0.0)
                , endAngle: CGFloat(2 * M_PI), clockwise: true)
            CGContextAddPath(context, path.CGPath)

就是用来在各条线之间画圆圈的。

以几乎略有不同的算法可以在calayer上绘制出CAGradientLayer的mask路线。也就是在core graphics里画得白线和在纸上铺上去的mask以后的gradient layer可以严丝合缝的组合在一起。这是看起来才能和苹果的health app一样的效果。这里需要说明,在添加了圆圈之后,每次画线的时候需要考虑要把线缩短。如果直接按照原来的方式的话,会优先穿过圆圈。

时间: 2024-08-06 03:43:54

swift:打造你自己的折线图的相关文章

用canvas绘制折线图

1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>用canvas绘制折线图</title> 6 </head> 7 <body> 8 <canvas id="cv"></canvas> 9 </body> 1

iOS:使用贝塞尔曲线绘制图表(折线图、柱状图、饼状图)

1.介绍: UIBezierPath :画贝塞尔曲线的path类 UIBezierPath定义 : 贝赛尔曲线的每一个顶点都有两个控制点,用于控制在该顶点两侧的曲线的弧度. 曲线的定义有四个点:起始点.终止点(也称锚点)以及两个相互分离的中间点. 滑动两个中间点,贝塞尔曲线的形状会发生变化. UIBezierPath :对象是CGPathRef数据类型的封装,可以方便的让我们画出 矩形 . 椭圆 或者 直线和曲线的组合形状 初始化方法: + (instancetype)bezierPath; /

Python中使用matplotlib 如何绘制折线图?

本文和大家分享的主要是python开发中matplotlib 绘制折线图相关内容,一起来看看吧,希望对大家学习和使用这部分内容有所帮助. matplotlib 1.安装matplotlib ① linux系统安装 # 安装matplotlib模块 $ sudo apt-get install python3-matplotlib# 如果是python2.7 执行如下命令 $ sudo apt-get install python-matplotlib# 如果你安装较新的Python,安装模块一乐

自定义view—折线图

学习导航 第一节:http://blog.csdn.net/bobo8945510/article/details/53197727 -自定义View-自定义属性及引用 第二节:http://blog.csdn.net/bobo8945510/article/details/53203233 自定义view02-图形绘制 第三节:http://blog.csdn.net/bobo8945510/article/details/53213938 自定义View-绘图基础之Path 第四节:http

手把手教你实现折线图之------安卓最好用的图表库hellocharts之最详细的使用介绍

因为项目需要搞一个折线图,按照日期显示相应的成绩,所以有了本文. 以前用过一次XCL-chart,但是感觉只适合固定图表,不去滑动的那种,因为你一滑动太卡了你懂得(毕竟作者好久没更新优化了),拙言大神我开玩笑的 ,毕竟我加你的群大半年了 - - 第二研究了一下achartenginee图表框架,一不美观,二 achartenginee的可定制性实在不敢恭维,做出来的图表根本不能满足需求 再试了一次网传最好用的MPchart和hellochart同一年出来的,但是要比hellochaet早点.说实

折线图 饼状图 柱状图

xaml 文件 <Window x:Class="Supplier.TrendCharts" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="产品价格趋势图" Height="500" Width

慕课网实战—《用组件方式开发 Web App全站 》笔记五-折线图组件开发

运用HTML5.CSS3.JS流行技术,采用组件式开发模式,开发Web App全站!技术大牛带你统统拿下不同类型的HTML5动态数据报告! <用组件方式开发 Web App全站 > 折现图绘制大致步骤 折线图画布 JavaScript CSS 折线图绘制网格线 // 水平网格线 100份 -> 10份 var step = 10; ctx.beginPath(); ctx.lineWidth = 1; ctx.strokeStyle = '#AAAAAA'; window.ctx = c

MindFusion--LineChart(折线图)

最近在统计一些信息,需要用一些折线图来体现以下,于是就学习了一下Mindfusion的这个第三方的空间,感觉效果还是不错的,这里就先简单的介绍一下他们的基本属性的用法吧. 因为我需要展示这些数据近些年来的走势的情况,所以用的是MindFusion的LineChart控件,但是倒是不难,找了几个简单的例子,把几个属性熟悉了一些,这里大概分为界面样式,数据源设置着两部分吧. 如果需要可以去这里边下载MindFusion:点击打开链接 界面样式 对于界面的样式,大概也就是搞清楚我们需要显示几部分数据,

ASP.NET实现折线图的绘制

用到.Net中绘图类,实现折线图的绘制,生成图片,在页面的显示,代码如下: 1 /// <summary> 2 /// 获取数据 3 /// strChartName:图名称: 4 /// yName:纵坐标名称: 5 /// xName:横坐标名称: 6 /// iyMaxValue:纵坐标最大值: 7 /// dyAveValue:纵坐标单位值=(纵坐标最大值/标量30) 8 /// ----100 30 :3 9 /// ----200 30 :1.5: 10 /// xdbColumn