前言
最近做的一個需求中,需要用旋轉的餅狀圖來展示數(shù)據(jù)。在github上找了一圈,發(fā)現(xiàn)竟然沒有現(xiàn)成的輪子,于是看了別人源碼后就自己實現(xiàn)了一下,代碼已經(jīng)放在我的github上。下面看看怎么完成這種效果。注意以下代碼是用swift實現(xiàn)的。
思路分析
看到下面這個效果,切入點應該是怎么生成這個餅狀圖,然后再讓餅狀圖旋轉。

怎么生成餅狀圖(UIBezierPath)
首先是要新建一個CAShapeLayer,這是一個繼承CALayer的類??赡苣銓ALayer比較陌生,不過做iOS一定不會對UIView陌生,每個UIView都有一個layer屬性,用來設置一些圓角、陰影等效果。至于為什么有UIView了,還要加一個layer,可以參考這本書iOS核心動畫高級技巧,layer的存在是為了讓OSX和iOS兩個平臺能兼容,在OSX中,NSView就對應了layer。
有了CAShapeLayer后,設置其幾個屬性:
- path,這里我們用到UIBezierPath,貝塞爾曲線來提供圓形軌跡。
- lineWidth: 圓環(huán)的寬度
- strokeColor:畫筆顏色
- strokeStart:畫筆起始點,取值范圍0.0-1.0
- strokeEnd:畫筆結束點,取值范圍0.0-1.0
單個餅狀圖的生成代碼如下:
private func generateLayers(radius:CGFloat, layerFrameWidth:CGFloat, percentageStart:CGFloat, percentageEnd:CGFloat) -> CAShapeLayer{
let path = UIBezierPath(arcCenter: CGPointMake(layerWidth, layerWidth), radius: radius, startAngle: CGFloat(-M_PI_2), endAngle: CGFloat(3 * M_PI_2) , clockwise: true)
let pieLayer = CAShapeLayer()
pieLayer.path = path.CGPath
pieLayer.lineWidth = lineWidth
pieLayer.strokeColor = UIColor(hue: percentageEnd, saturation: 0.5, brightness: 0.75, alpha: 1.0).CGColor
pieLayer.fillColor = nil
pieLayer.strokeStart = percentageStart
pieLayer.strokeEnd = percentageEnd
return pieLayer
}
可以看到,通過指定貝塞爾曲線的圓心arcCenter,半徑radius,開始角度startAngle、結束角度endAngle、繪制方向clockwise來生成一個軌跡path。
然后就是通過指定畫筆的顏色strokeColor和寬度lineWidth以及起始點位置strokeStart和結束位置strokeEnd,來畫出一部分圓環(huán)(餅狀圖)。
不得不說UIBezierPath真是特別好用,因為我剛開始不懂時,嘗試用CoreGraphics去畫,那感覺真是想死。
全部的餅狀圖拼接在一起,就成了一個圓環(huán),代碼如下
private func setupRotateLayers(){
containerLayer = CAShapeLayer()
containerLayer.frame = CGRectMake(0, 0, self.bounds.width, self.bounds.width)
var percentageStart:CGFloat = 0
var percentageEnd:CGFloat = 0
for i in 0...dataItem.count - 1{
percentageEnd += dataItem[i] / itemValueAmount
let pieLayer = generateLayers(radius, layerFrameWidth: layerWidth, percentageStart: percentageStart, percentageEnd: percentageEnd)
containerLayer.addSublayer(pieLayer)
percentageStart = percentageEnd
}
gradientMask(radius, width: layerWidth)
if dataItem.count > 0{
let initRotateRadian = -CGFloat(M_PI) * dataItem[0] / itemValueAmount
rotateContainerLayerWithRadian(initRotateRadian)
}
self.layer.addSublayer(containerLayer)
}
首先需要一個容器container來收集這些小塊的餅狀圖,所以先是生成了一個containerLayer,然后根據(jù)外部傳入的數(shù)據(jù)dataItem來計算每個餅狀圖的所占比例percentageStart和percentageEnd,并轉化為餅狀圖的開始角度和結束角度。
后面的gradienMask用來做一個漸變顯示的效果,接下去的代碼是初始化旋轉的角度。
餅狀圖的旋轉(CoreAnimation)
?旋轉一個layer,我最開始想用重繪來實現(xiàn),后面發(fā)現(xiàn)用CoreAnimation似乎更簡單。
代碼示例如下
func reDraw(index:Int){
var curIndex = index - 1
if index == 0{
curIndex = dataItem.count - 1
}
var rotateRadian = dataItem[curIndex] / itemValueAmount + dataItem[index] / itemValueAmount
rotateRadian = -rotateRadian * CGFloat(M_PI)
rotateContainerLayerWithRadian(rotateRadian)
}
private func rotateContainerLayerWithRadian(radian:CGFloat){
let myAnimation = CABasicAnimation(keyPath: "transform.rotation")
let myRotationTransform = CATransform3DRotate(containerLayer.transform, radian, 0, 0, 1)
if let rotationAtStart = containerLayer.valueForKeyPath("transform.rotation") {
myAnimation.fromValue = rotationAtStart.floatValue
myAnimation.toValue = CGFloat(rotationAtStart.floatValue) + radian
}
containerLayer.transform = myRotationTransform
myAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
containerLayer.addAnimation(myAnimation, forKey: "transform.rotation")
}
?旋轉的時候會用一個index來做標記,記住當前的位置,所以調用reDraw時傳入下一個index就可以完成旋轉到下一個餅狀圖的操作。
?可以看到旋轉角度的計算是rotateRadian = dataItem[curIndex] / itemValueAmount + dataItem[index] / itemValueAmount,需要取數(shù)組左右元素值的各一半。然后傳入rotateContainerLayerWithRadian函數(shù),通過這個函數(shù)來執(zhí)行旋轉操作。
?myAnimation是用CABasicAnimation創(chuàng)建的一個動畫,這里要特別留意其參數(shù)keyPath,我以前學CoreAnimation時對這個參數(shù)不以為意,然后怎么調試效果都出不來,白白浪費了好多時間。其實這個transform.rotation可以在蘋果的官方文檔中查到,特指layer旋轉的屬性。
?然后用CATransform3DRotate來創(chuàng)建一個transform,直接賦值給containerLayer.transform就可以了,留意一下角度換算就沒問題了。
?fromValue和toValue應該是最熟悉的兩個屬性了,fromValue表示動畫開始前的屬性狀態(tài),toValue表示動畫結束后的屬性狀態(tài)。
?可能你會對timingFunction感興趣,這個效果可以在這里得到解釋。
?最后將配置好的myAnimation添加到containeLayer就OK了,系統(tǒng)會自動去執(zhí)行這一部分動畫的。
后話
這部分代碼僅分享了一種實現(xiàn)方法,后期還會繼續(xù)改進,比如傳入的不只是dataItem,還應該有自定義的color,以及每一部分的百分比,旋轉開始位置等等。