iOS——用可旋轉的餅狀圖來展示數(shù)據(jù)

前言

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

思路分析

看到下面這個效果,切入點應該是怎么生成這個餅狀圖,然后再讓餅狀圖旋轉

rotatePieChartDemo.gif

怎么生成餅狀圖(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來計算每個餅狀圖的所占比例percentageStartpercentageEnd,并轉化為餅狀圖的開始角度和結束角度。
后面的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就可以了,留意一下角度換算就沒問題了。
?fromValuetoValue應該是最熟悉的兩個屬性了,fromValue表示動畫開始前的屬性狀態(tài),toValue表示動畫結束后的屬性狀態(tài)。
?可能你會對timingFunction感興趣,這個效果可以在這里得到解釋。
?最后將配置好的myAnimation添加到containeLayer就OK了,系統(tǒng)會自動去執(zhí)行這一部分動畫的。

后話

這部分代碼僅分享了一種實現(xiàn)方法,后期還會繼續(xù)改進,比如傳入的不只是dataItem,還應該有自定義的color,以及每一部分的百分比,旋轉開始位置等等。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容