PNChart 原始碼解析

J_Knight_發表於2017-12-22

一. 框架介紹

PNChart是國內開發者開發的iOS圖表框架,現在已經7900多顆star了。它涵蓋了折線圖,餅圖,散點圖等圖表。圖表的可定製性很高,而且UI設計簡潔大方。

該框架分為兩層:檢視層和資料層。檢視層裡有兩層繼承關係,第一層是所有型別圖表的父類PNGenericChart,第二層就是所有型別的圖表。提供一張圖來直觀感受一下:

11859001-8be6cd678806d45e
層級圖

在這張圖裡,需要注意以下幾點:

  1. 帶箭頭的線和不帶箭頭的線的區別。
  2. Data類對應圖表的一組資料,因為當前型別的圖表支援多組資料(例如:餅狀圖沒有Data類,因為餅狀圖沒有多組資料,而折線圖LineChart是支援多組資料的,所以有Data類。
  3. Item類負責將傳入圖表的某個真實值轉化為圖表中顯示的值,具體做法會在下文詳細講解。
  4. BarChart類裡面的每一根柱子都是PNBar的例項(該型別的圖表不在本篇講解的範圍之內)。

今天就來介紹一下該框架裡的折線圖。一旦學會了折線圖的繪製,瞭解了繪圖原理,那麼其他型別的圖表就可以觸類旁通。

上文提到過,該框架的折線圖是支援多組資料的,也就是在同一張圖表上顯示多條折線。先帶大家看一下效果圖:

12859001-7357e47c2b0b062b
折線圖

折線圖在效果上還是很簡潔美觀的(並支援動畫效果),如果現在的你還不知道如何使用CAShapeLayerUIBezierPath畫圖並附加動畫效果,那麼本篇原始碼解析非常適合你。

閱讀本文之後,你可以掌握有關圖形繪製的相關知識,也可以掌握自定義各種圖形(UIView)的方法,而且你也應該有能力作出這樣的圖表,甚至更好!

在開始講解之前,我先粗略介紹一下利用CAShapeLayer畫圖的過程。這個過程有三個大前提:

  • 因為UIView是對CALayer的封裝,所以我們可以通過改變UIView所持有的layer屬性來直接改變UIView的顯示效果。
  • CAShapeLayerCALayer的子類。
  • CAShapeLayer的使用是依賴於UIBezierPath的。UIBezierPath就是“路徑”,可以理解為形狀。不難理解,想象一下,如果我們想畫一個圖形,那麼這個圖形的形狀(包括顏色)是必不可少的,而這個角色,就需要UIBezierPath來充當。

那麼了這三個大前提,我們就可以知道如何畫圖了:

  1. 例項化一個UIBezierPath,並賦給CAShapeLayer例項的path屬性。
  2. 將這個CAShapeLayer的例項新增到UIViewlayer上。

簡單的程式碼演示上述過程:

現在大致瞭解了畫圖的過程,我們來看一下該框架的作者是如何實現一個折線圖的吧!

二. 原始碼解析

首先看一下整個繪製折線圖的步驟:

  1. 圖表的初始化。
  2. 獲取橫軸和縱軸的資料。
  3. 計算折線上所有拐點的x,y值。
  4. 計算每個拐點中間的圓圈的貝塞爾曲線(UIBezierPath)。
  5. 生成每個拐點上面的Label(可有可無)。
  6. 計算每條線段的貝塞爾曲線(UIBezierPath)。
  7. 將上面得到的貝塞爾曲線賦給每條線段和圓圈的layer(CAShapeLayer)。
  8. 繪製所有折線(所有線段+所有圓圈)。
  9. 新增動畫(可有可無)。
  10. 繪製x,y座標軸。

在集合程式碼具體講解之前,我們要清楚三點(非常非常重要):

  1. 此折線圖框架是可以設定拐點的樣式的:可以設定為沒有樣式,也可以設定有樣式:圓圈,方塊,三角形。
    • 如果沒有樣式,則是簡單的線段與線段的連線,在拐點處沒有任何其他控制元件。
    • 如果是有樣式的,那麼這條折線裡的每條線段(在本篇文章裡統一說成線段)之間是分離的,因為線段中間有一個拐點控制元件。本篇文章介紹的是圓圈樣式(如上圖所示,拐點控制元件是一個圓圈)。
  2. 上文提到過,該折線圖框架可以在一張圖表裡同時顯示多條折線,也就是可以設定多組資料(一條折線對應一組資料)。因此,上面的3,4,5,6,7項都是用各自不同的一個陣列儲存的,陣列裡的每一個元素對應一條折線的資料。
  3. 既然同一個張圖表可以顯示多條折線:
    • 那麼有些屬性就是這些折線共有的,比如橫座標的value,這些屬性儲存在PNLineChart的例項裡面。
    • 有些屬性是每條折線私有的,比如每條折線的顏色,縱座標value等等,這些屬性儲存在PNLineChartData裡面。每一條折線對應一個PNLineChartData例項。這些例項彙總到一個陣列裡面,這個陣列由PNLineChart的例項管理。

在充分了解了這三點之後,我們結合一下程式碼來看一下具體的實現:

1. 圖表的初始化

上面這段程式碼我刻意省去了其他一些基本的設定,突出了圖表佈局的設定。

佈局的設定是圖表繪製的前提,因為在最開始的時候,就應該計算出“畫布”,也就是圖表內容(不包括座標軸和座標label)的具體大小和位置(內邊距以內的部分)。

在這裡,我們需要獲取真正繪製圖表的畫布的寬高(_chartCavanWidth_chartCavanHeight)。而且,要留意的是_chartMarginLeft在將來是要用作y軸Label的寬度,而_chartMarginBottom在將來是要用作x軸Label的高度的。

用一張圖直觀看一下:

13859001-958e7d4172c18f55
整個控制元件的大小和畫布的大小

2. 獲取橫軸和縱軸的資料

現在畫布的位置和大小確定了,我們可以來看一下折線圖是怎麼畫的了。
整個圖表的繪製都基於三組資料(也可以是兩組,為什麼是兩組,我稍後會給出解釋),在講解該框架是如何利用這些資料之前,我們來看一下這些資料是如何傳進圖表的:

上面的程式碼我可以略去了很多多餘的設定,目的是突出圖表資料的設定。

不難看出,這裡有三個資料傳給了lineChart:

1.x軸的資料:

這段程式碼呼叫之後,實現了:

  1. 根據傳入的xLabel陣列裡元素的數量,內容寬度(_chartCavanWidth)和下邊距(_chartMarginBottom),計算每個xlabel的size。
  2. 根據xLabel所需要展示的內容(NSString)和寬度,例項化所有的xLabel(包括內容,位置)並顯示出來,最後儲存在_xChartLabels裡面。

2.y軸的資料:

這段程式碼呼叫之後,實現了:

  1. 根據傳入的yLabel陣列裡元素的數量,內容高度(_chartCavanHeight)和左邊距(_chartMarginLeft),計算出每個ylabel的size。
  2. 根據xLabel所需要展示的內容(NSString)和寬度,例項化所有的yLabel(包括內容,位置)並顯示出來,最後儲存在_yChartLabels裡面。

3.一條折線上每個點的實際值:

著重講一下block:為什麼不直接把這個陣列(dataArray)作為line chart的屬性傳進去呢?我認為作者是想提供一個介面給使用者一個自己轉化y值的機會。

像上文所說的,這裡1,2是屬於lineChart的資料,它適用於這張圖表上所有的折線的。而3是屬於某一條折線的。

現在回答一下為什麼可以只傳入兩組資料:因為y軸資料可以由每個點的實際值陣列得出。可以簡單想一下,我們可以獲取這些真實值裡面的最大值,然後將它n等分,就自然得到了y軸資料了。

我們已經佈局了x軸和y軸的所有label,現在開始真正計算圖表的資料了。

注意:下面要介紹的3,4,5,6項都是在同一方法中計算出來,為了避免程式碼過長,我將每個部分分解開來做出解釋。因為在同一方法裡,所以這些涉及到for迴圈的語句是一致的。

整個圖表的繪製都是依賴於資料的處理,所以3,4,5,6項也是理解該框架的一個關鍵!

首先,我們需要計算每個資料點(拐點)的準確位置:

3. 計算折線上所有拐點的x,y值。

在這裡需要注意兩點:

  1. 這裡的pathPoints對應的是lineChart_pathPoints屬性。它是一個二維陣列,儲存每條折線上所有點的CGPoint
  2. y值的計算:是需要從y的真實值轉化為這個拐點在圖表裡的y座標,轉化方法的實現(仔細看幾遍就懂了):

4. 計算每個拐點中間的圓圈的貝塞爾曲線(UIBezierPath)

在這裡,pointsPath對應的是lineChart_pointsPath屬性。它是一個一維陣列,儲存每條折線上的圓圈貝塞爾曲線(UIBezierPath)。

5. 生成每個拐點上面的Label(可有可無)

注意,在這裡,這些label的實現是通過一個CATextLayer實現的,並不是生成一個個Label放在陣列裡儲存,具體實現方法如下:

6. 計算每條線段的貝塞爾曲線(UIBezierPath)

7. 將上面得到的貝塞爾曲線賦給每條線段和圓圈的layer(CAShapeLayer)。

7.1 所有線段的layer:

7.2 所有圓圈的layer:

注意,這裡並沒有將所有圓圈的UIBezierPath賦給對應的layer,而是在下一步,繪圖的時候做的。

8.繪製所有折線(所有線段+所有圓圈)&& 9. 新增動畫

這裡要注意兩點:

1.如果想給layer新增動畫,只需要例項化一個animation(在這裡是CABasicAnimation)並呼叫layer的addAnimation:方法即可。我們看一下關於CABasicAnimation的例項化程式碼:

2.在這裡呼叫了setNeedsDisplay方法之後,會呼叫drawRect:方法,在這個方法裡,完成了x,y座標軸的繪製:

10.繪製x,y座標軸

到這裡,一張完整的圖表就可以畫出來了。但是當前繪製的圖表的折線都是直線,在上面還展示了一張曲線圖。那麼如果想繪製帶有曲線的折線圖應該怎麼做呢?對,就是在貝塞爾曲線上下功夫。

當我們獲取了所有線段的端點陣列後,我們可以通過他們繪製彎曲的貝塞爾曲線(注意:該方法是對應上面對第6項的下半部分:生成每一個線段對貝塞爾曲線):

注意一下生成彎曲的貝塞爾曲線的方法:controlPointBetweenPoint1:andPoint2:

OK,這樣一來,直線的曲線圖還有曲線的曲線圖就大概掌握了。不過還差一個東西,就是圖表對點選的響應。

我們需要思考一下:既然一張圖表裡可以顯示多條折線,所以,當手指點選圖表上的點以後,應該同時返回兩個資料:

  1. 點選了哪條折線上的這個點。
  2. 點選了這條折線上的哪個點。

該框架的作者很好地完成了這兩個任務,我們來看一下他是如何實現的:

響應點選的代理方法

點選了哪條折線的判斷

點選了哪個點的判斷

這下就完整了,一個帶有響應功能的圖表就做好啦!

關於自定義UIView

這裡只是將圖表的layer加在了UIView的layer上,那如果想完全自定義view的話,只需將圖表的layer完全賦給UIView的layer即可,這樣一來,想要畫出任意形狀的UIView都可以。

三. 最後的話

關於圖表的繪製,相對貝塞爾曲線與CALayer來說,資料的處理是一個比較麻煩的點。但是一旦學會了折線圖的繪製,瞭解了繪圖原理,那麼其他型別的圖表就可以觸類旁通。

 

相關文章