swift:打造你自己的折线图
看到蘋果Health里的折線圖了嗎。我們就是要打造一個(gè)這樣的折線圖。沒(méi)看過(guò)的請(qǐng)看下圖。
我們的主題在于折線圖本身。其他的包括步數(shù)、日平均值等描述類的內(nèi)容這里就不涉及了。
?
首先觀察,這個(gè)圖種包含些什么組成部分。線?這個(gè)太明顯都看見(jiàn)了。還有每個(gè)節(jié)點(diǎn)的小圓圈,還有折線圖里從上到下的漸變。這里是白色的從上到下逐漸透明的效果。還有一條虛線。這個(gè)暫時(shí)先不考慮了。你能繪制出來(lái)最下面的x軸標(biāo)尺,繪制個(gè)虛線還不是小菜?
為什么說(shuō)是繪制呢,因?yàn)轱@然我們不想用一個(gè)UIView把像素設(shè)置為1,背景色設(shè)置為UIColor.whiteColor(),然后設(shè)置View 的傾斜度的方式來(lái)堆砌這個(gè)line chat。首先必須嚴(yán)重的鄙視這種做法。在開(kāi)發(fā)中不能光是把各種UIButton、UILabel什么的設(shè)定好了frame就網(wǎng)上沒(méi)完沒(méi)了的堆。或者更有 甚者直接拖動(dòng)這些控件到Storyboard上。擺個(gè)位置,設(shè)置個(gè)寬和高別的就完全不管了。autolayout什么的一概不問(wèn),使用了 storyboard也適配不了多分辨率。這樣的結(jié)果是誰(shuí)維護(hù)代碼誰(shuí)遭殃。
正確的做法是提升代碼。有多個(gè)地方都用到同樣的組合控件的時(shí)候,比如多選框、單選框,就自定義一個(gè)。這樣,至少可以達(dá)到一改全改的效果。代碼維護(hù)簡(jiǎn) 單了很多。同時(shí)需要考慮效率的問(wèn)題。比如我們的line chart,就使用Core Graphics和QuartzCore框架中的CAShapeLayer繪制。這樣執(zhí)行效率明顯比堆砌UIView的方法效率高--占用資源少,執(zhí)行 快。
看看CALayer的定義:
class CALayer : NSObject, NSCoding, CAMediaTiming再看看UIView的定義:
class UIView : UIResponder, NSCoding, UIAppearance, NSObjectProtocol, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace你就應(yīng)該知道為什么完全不能用UIView來(lái)堆砌這個(gè)圖了。
言歸正傳!畫(huà)線可以用Core Graphics一點(diǎn)點(diǎn)的畫(huà),也可以用CALayer來(lái)話,準(zhǔn)確的說(shuō)是CAShapeLayer更方便,所以我們用CAShapeLayer來(lái)畫(huà)線。用 CAShapeLayer畫(huà)線灰常之簡(jiǎn)單。總的來(lái)說(shuō)就是設(shè)定路線(Path),然后把這個(gè)路線賦值給這個(gè)layer畫(huà)線就完成了。比如,初始化一條貝塞爾 曲線,然后指定好center point和半徑,起始角度和結(jié)束角度,然后“BANG”。“BANG”是一個(gè)象聲詞,龍珠里很多。指定你的CAShapeLayer實(shí)例的path屬性 值為這個(gè)path。此處略去一堆什么給你的view.layer.addsublayer什么的細(xì)節(jié)。運(yùn)行后你就會(huì)看到一個(gè)從起始角度到結(jié)束角度的一個(gè)半 圓。
運(yùn)行起來(lái)之后,你會(huì)看到這個(gè)半圓和你需要的起始角度、結(jié)束角度差很多。所以,還是畫(huà)一個(gè)正圓比較容易一些。尤其現(xiàn)在我們才剛剛開(kāi)始接觸這個(gè)神秘的東 東。等下還有更神秘的。。。要畫(huà)正圓只要指定起始角度為0(這里需要嚴(yán)重說(shuō)明一下,角度都是弧度制的,比如,π、2π什么的)。結(jié)束角度為2π,也就是(M_PI * 2)。半徑隨便,圓心最好設(shè)定在屏幕的中心,也就是:
UIScreen.mainScreen().bounds.height /?2和UIScreen.mainScreen().bounds.width /?2。這樣就是在屏幕中心點(diǎn),以你給定的值為半徑畫(huà)了一個(gè)圓圈。效果如圖:
給的貝塞爾曲線是這樣的:
UIBezierPath(arcCenter: centerPoint, radius: CGRectGetWidth(bounds) / 2 - 30.0, startAngle: 0, endAngle: CGFloat(M_PI * 2.0), clockwise: true).CGPath這里需要注意的是一定要在最后調(diào)用屬性CGPath,這個(gè)才是CAShapeLayer可以接受的Path的類型。直接賦值是會(huì)報(bào)錯(cuò)的。在貝塞爾曲 線初始化的過(guò)程中角度值需要使用CGFloat類型。M_PI是Double類型的。這里需要類型轉(zhuǎn)換一下。否則報(bào)錯(cuò)會(huì)報(bào)在radius的身上,但是起始 是角度的類型問(wèn)題。
圓是畫(huà)出來(lái)了,但是我們要繪制的是line chart,是直線。該如何解決呢。這里就需要說(shuō)明一下繪制線的一般感性認(rèn)識(shí)。首先CAShapeLayer需要知道繪制的起始點(diǎn)在哪里,其次,從哪一點(diǎn) 到哪一點(diǎn)繪制一條線。對(duì)于圓的貝塞爾曲線來(lái)說(shuō)自然是從角度為0的,半徑長(zhǎng)度和圓心來(lái)開(kāi)始畫(huà)線,線一直延續(xù)到結(jié)束角度2π(PI)。對(duì)于一條直線就簡(jiǎn)單多 了。起點(diǎn)是指定的一個(gè)點(diǎn)。然后,添加一條線到另一個(gè)點(diǎn)。來(lái)看看如何具體的用代碼畫(huà)一條線。
var path = CGPathCreateMutable()var x = UIScreen.mainScreen().bounds.width / 2, y = UIScreen.mainScreen().bounds.height / 5CGPathMoveToPoint(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線就是這么畫(huà)出來(lái)的。有線了以后就需要考慮另一個(gè)問(wèn)題了,線下面的漸變色。這個(gè)就需要用到另一種Layer:CAGradientLayer。CAGradientLayer有一個(gè)屬性可以做到這一點(diǎn),這個(gè)屬性就是colors。給這個(gè)屬性多少顏色,CAGradientLayer就會(huì)出現(xiàn)多少?gòu)囊粋€(gè)顏色到另一個(gè)顏色的漸變。注意一點(diǎn),這里需要的顏色都是UIColor.yellowColor().CGColor。看到這個(gè)CGColor了嗎?一定要這個(gè)顏色才行。否則,不報(bào)錯(cuò),也不顯示任何的顏色!
代碼:
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)這效果就出來(lái)了:
到這里你應(yīng)該就明白了。圖一種的白色到透明的漸變其實(shí)就是不同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]看效果,白色從上到下的漸變填充已經(jīng)出來(lái)了。畫(huà)線前面已經(jīng)講過(guò)。現(xiàn)在的問(wèn)題就是讓這個(gè)填充按照畫(huà)得線剪裁。這個(gè)非常簡(jiǎn)單。
我們來(lái)給上面的CAShapeLayer這樣的一個(gè)路線:
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屬性為這個(gè)CAShapeLayer。
gradientLayer.mask = progressLayer這樣一來(lái)。效果就出來(lái)了。
但是。。仔細(xì)一個(gè),填充的漸變白色圖是有了,那么線呢?白色的線沒(méi)有。CAShapeLayer的線最終都只是成為CAGradientLayer的剪裁線。要解決這個(gè)問(wèn)題就要上下面的重頭戲了。
為了解決這個(gè)問(wèn)題,我們不得不祭出Core Graphics神器了。總體的構(gòu)造思路是在Controller中添加一個(gè)View,在這個(gè)View中使用Core Graphics來(lái)畫(huà)線,之后在上面添加我們上文說(shuō)到的兩個(gè)Layer。也就是下面畫(huà)線,然后用Layer來(lái)完成漸變色的填充和對(duì)這個(gè)填充色的剪裁。
Core Graphics畫(huà)線比CALayer還是麻煩一些的,但是思路總體上一致。也是把畫(huà)筆放到起始點(diǎn)(在哪里開(kāi)始畫(huà)線)。之后也是從哪里到哪里畫(huà)線。總體來(lái)說(shuō),畫(huà)線的思路就是這樣。
首先,需要在Core Graphics中鋪上畫(huà)布:
var context = UIGraphicsGetCurrentContext()2. 指定線的顏色和線的寬度:
CGContextSetStrokeColorWithColor(context, UIColor.whiteColor().CGColor) CGContextSetLineWidth(context, 1.0)3. 開(kāi)始畫(huà)線:
CGContextMoveToPoint(context, kBottomMargin, CGRectGetHeight(rect) - kBottomMargin) CGContextAddLineToPoint(context, CGRectGetWidth(rect) - kBottomMargin, CGRectGetHeight(rect) - kBottomMargin)這里必須補(bǔ)充一點(diǎn)。在畫(huà)線的時(shí)候,我們需要一些列的點(diǎn)坐標(biāo)。暫時(shí),只是用模擬的方式實(shí)現(xiàn)。var x = calculateX(0)和var y = calculateY(0)就是第一個(gè)點(diǎn)得x,y坐標(biāo)的計(jì)算方法。具體的代碼在后面。這些給定的點(diǎn)需要映射到你的畫(huà)布的坐標(biāo)系中。calculateX、Y就是做這個(gè)映射的。雖然省略了一些步驟。但是你應(yīng)該可以從初中的數(shù)學(xué)基礎(chǔ)中明白這個(gè)是怎么回事的,所以此處只做解釋其他省略。
func calculateX(i: Int) -> CGFloat {var x = kBottomMargin + CGFloat(i) * kUnitLabelWidth!return x}kBottomMargin是x點(diǎn)在左側(cè)的一個(gè)margin。只是展示需要,不用關(guān)心。?CGFloat(i) * kUnitLabelWidth!,i是第幾個(gè)點(diǎn),也就是x軸上的index。kUnitLabelWidth!是x軸上兩點(diǎn)之間的距離,至于感嘆號(hào)就不多解釋了,那個(gè)是swift的基礎(chǔ)。
?
func calculateY(i: Int) -> CGFloat {var y: CGFloat = 0switch(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 }這里主要計(jì)算,每個(gè)x點(diǎn)對(duì)應(yīng)的y點(diǎn)(這里就摸你了y值對(duì)應(yīng)在畫(huà)布坐標(biāo)系的方法)。
有了以上的只是就可以畫(huà)出折線圖了。具體的方法如下:
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)就是用來(lái)在各條線之間畫(huà)圓圈的。
以幾乎略有不同的算法可以在calayer上繪制出CAGradientLayer的mask路線。也就是在core graphics里畫(huà)得白線和在紙上鋪上去的mask以后的gradient layer可以嚴(yán)絲合縫的組合在一起。這是看起來(lái)才能和蘋果的health app一樣的效果。這里需要說(shuō)明,在添加了圓圈之后,每次畫(huà)線的時(shí)候需要考慮要把線縮短。如果直接按照原來(lái)的方式的話,會(huì)優(yōu)先穿過(guò)圓圈。
轉(zhuǎn)載于:https://www.cnblogs.com/Free-Thinker/p/4946357.html
總結(jié)
以上是生活随笔為你收集整理的swift:打造你自己的折线图的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 随着互联网的深化,世间万物都将如何学会思
- 下一篇: 【GPS】GPS的C_GNSS_RF_E