MPAndroidChart使用詳解和擴展

索引

基礎使用(靜態圖表)

定義

繪製一張圖,包含線形圖,餅圖,柱狀圖等。可以設置各類配置,如顏色,字體大小,軸樣式,數據格式化等等。canvas

使用流程

能夠經過繼承和組合的方式選擇具體的圖表類型進行使用。若是將圖表抽象成一個View來看,整個流程會比較清晰。大體分爲如下幾個步驟bash

  1. 基礎配置:在初始化階段作一些相似touch、drag、sacle屬性的開關控制,X.Y軸、Legend、Description等的初始化設置(具體方法需看源碼)
  2. 數據處理:將接口數據轉換爲圖表須要的xxxEntry對象。而後進一步將其轉換爲xxxDataSet對象,併爲其設置樣式,最後合併成相應圖表須要的xxxData。ChartData 與 DataSet的對應關係爲 一對多 。DataSet 與 Entry的對應關係一樣爲 一對多
  3. 圖表繪製:爲Chart設置ChartData,使其重繪

若是對MP的Api使用有必定的瞭解後,經過以上的步驟就能輕鬆的實現一張圖的繪製。架構

代碼示例:ide

Step1:

 private void initChart() {
        mLineChart.setTouchEnabled(false);
        mLineChart.setDragEnabled(false);
        mLineChart.setScaleEnabled(false);

        YAxis rightAxis = mLineChart.getAxisRight();
        rightAxis.setEnabled(false);

        XAxis xAxis = mLineChart.getXAxis();
        xAxis.setDrawGridLines(false);
        xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
        xAxis.setDrawLabels(true);
        xAxis.setAvoidFirstLastClipping(false);
        xAxis.setGranularityEnabled(true);
        xAxis.setValueFormatter(xAxisFormatter);
        xAxis.setYOffset(4f);
        .....
}

Step2:

    private LineData generateLineData(XXModel model) {
        List<List<Entry>> lists = model.getLineDatas();
        List<ILineDataSet> lineDataSets = new ArrayList<>();
        for (int i = 0; i < lists.size(); i++) {
            LineDataSet dataSet = new LineDataSet(lists.get(i), "LineDataSet Num:" + i);
            dataSet.setAxisDependency(YAxis.AxisDependency.LEFT);
            dataSet.setDrawValues(false);
            dataSet.setDrawCircles(true);
            dataSet.setDrawCircleHole(false);
            dataSet.setCircleRadius(lineCircleRadius);
            dataSet.setLineWidth(lineWidth);
            dataSet.setValueTextSize(lineTextSize);
            lineDataSets.add(dataSet);
        }
        return new LineData(lineDataSets);
    }


Step3:

        LineData lineData = generateLineData(model);
        mLineChart.setData(lineData);
        mLineChart.postInvalidate();

複製代碼

數據更新(動態圖表)

經過以上流程咱們可使用MP作出一張圖表。可是若是可能咱們的數據不是一成不變的。可能隨着時間推移,數據量增大。或者隨着手勢滑動,須要加載新的數據。咱們能夠經過兩種方式動態更新數據(僅僅是據我所知):post

  • 使用MP自帶的Api進行單個Entry的增刪或者單個DataSet的增刪
  • 直接全量重置setData

說到底就是就是讓Chart從新繪製了一次。具體怎樣根據需求增長/更新數據纔是重點。對於Chart來講,僅僅是重繪操做。學習

圖表設置

關於圖表具體的配置選項主要能夠分爲三類:字體

  • 針對圖表總體的通用和特殊設置
  • 針對於Axis(軸)、Legend、LimitLine、Description等組件配置
  • 針對數據顯示的配置,xxDataSet/ChartDat

瞭解相關具體的配置須要深刻其中查看詳細的源碼。而對Api的源碼有必定的瞭解後,會爲後面對MP的擴展打好基礎。由於圖表最終的繪製過程,都須要結合各類配置條件進行相應的繪製。關於MP中經常使用的屬性設置和方式作了一份整理,詳細狀況能夠了解這裏ui

庖丁解牛

知道了如何使用MP,以及大體結構後。接下來即是進一步分析MP的繪製l流程。在次以前,咱們先思考幾個問題,而後帶着這幾個問題繼續往下研:this

  1. MP的架構設計是怎樣的
  2. 設置的屬性(什麼時候)起到什麼做用
  3. 數據是經過怎樣的方式映射爲視圖View的

首先咱們不要被「圖表」這個詞誤導,說到底也就是一個View視圖。Chart自己只是一個ViewGroup。只不過是由許多部分一一組成的。粗略的畫了張原型圖:spa

MP原型分析.png

從原型圖來看,Chart自己是個大容器或者說是一個組合體。大體由組件和內容兩部分組成。組件主要包括XY軸、Legend、限制線等。組件能夠單獨進行設置。內容主要包括圖表自己和數據的渲染。Chart包含一個或者多個組件,Chart會在適當的時機(實際計算/繪製的時候)通知組件作相應的操做。從前面的代碼使用示例中也能夠看出這一點。

其次要明確一個概念,Chart自己「不作任何計算和繪製的操做」。這裏之因此用雙引號的緣由是由於在面向對象的思想上Chart只作事件執行的分發者,具體的數據計算、數據與像素位置轉換、內容繪製等操做都是由另外的對象執行(對應MP中的各類Renderer)。有了以上的概念之後,來分析從setData到Chart到呈現到視圖上之間的整個過程,直接上圖:

MPAndroidChart數據渲染時序圖.png

圖中的BarLineChartBase是Chart的直接實現類。看圖可能比較懵逼,我大體的梳理成幾個流程:

  1. Chart調起自身的notifyDataSetChange從而使得Chart知道數據發生了變化
  2. 通知組件從新執行計算操做。主要是XY軸組件計算起極值和區間。
  3. onDraw()方法中調用各個Renderer執行繪製操做

根據時序圖和以上的流程咱們再來看開始提出的幾個問題

數據是經過怎樣的方式映射爲視圖View的

其實最後的繪製操做仍是調用系統Canvas提供的一系列繪製方法完成(主要圖中藍色流程線),因此對於這個問題的疑惑點更應該是,從接口拿到一堆數據,圖表是怎麼知道要繪製在哪裏的,對應到手機座標系中的哪一個位置的。 注意圖中的兩條綠色流程線(ps:不一樣類型的操做特地作了顏色區分,良心吧)。calcMinMax()的實際做用上是通知XY軸從新計算起最大最小值和區間range。

@Override
    protected void calcMinMax() {

        mXAxis.calculate(mData.getXMin(), mData.getXMax());

        // calculate axis range (min / max) according to provided data
        mAxisLeft.calculate(mData.getYMin(AxisDependency.LEFT), mData.getYMax(AxisDependency.LEFT));
        mAxisRight.calculate(mData.getYMin(AxisDependency.RIGHT), mData.getYMax(AxisDependency
                .RIGHT));
    }
    
        public void calculate(float dataMin, float dataMax) {

        // if custom, use value as is, else use data value
        float min = mCustomAxisMin ? mAxisMinimum : (dataMin - mSpaceMin);
        float max = mCustomAxisMax ? mAxisMaximum : (dataMax + mSpaceMax);

        // temporary range (before calculations)
        float range = Math.abs(max - min);

        // in case all values are equal
        if (range == 0f) {
            max = max + 1f;
            min = min - 1f;
        }

        this.mAxisMinimum = min;
        this.mAxisMaximum = max;

        // actual range
        this.mAxisRange = Math.abs(max - min);
    }

複製代碼

而calculateOffsets()作了兩件事:

  1. 從新計算Chart的內容大小
  2. 並計算好數據和像素的縮放比例

說到這裏不得不提MP中一個十分重要的類ViewPortHandler.咱們能夠將ViewPortHandler理解爲一個內存區域。Chart將自身一個屬性,好比高寬、大小、縮放比等,存在這個「內存」中。其餘對象想獲取這些屬性,經過ViewPortHandler就能夠獲取到。

爲何要畫蛇添足使用ViewPortHandler來存儲這些信息呢?還記得前面說過的MP是一個組合體嗎,可能多個地方都須要使用到這些屬性,而ViewPortHandler正好保證了多個對象獲取到的Chart屬性是一致的。

回過頭來繼續看calculateOffsets()作了那些事。直接看代碼:

@Override
    public void calculateOffsets() {

        if (!mCustomViewPortEnabled) {

            float offsetLeft = 0f, offsetRight = 0f, offsetTop = 0f, offsetBottom = 0f;

            calculateLegendOffsets(mOffsetsBuffer);

            offsetLeft += mOffsetsBuffer.left;
            offsetTop += mOffsetsBuffer.top;
            offsetRight += mOffsetsBuffer.right;
            offsetBottom += mOffsetsBuffer.bottom;

            // offsets for y-labels
            if (mAxisLeft.needsOffset()) {
                offsetLeft += mAxisLeft.getRequiredWidthSpace(mAxisRendererLeft
                        .getPaintAxisLabels());
            }

            if (mAxisRight.needsOffset()) {
                offsetRight += mAxisRight.getRequiredWidthSpace(mAxisRendererRight
                        .getPaintAxisLabels());
            }

            if (mXAxis.isEnabled() && mXAxis.isDrawLabelsEnabled()) {

                float xLabelHeight = mXAxis.mLabelRotatedHeight + mXAxis.getYOffset();

                // offsets for x-labels
                if (mXAxis.getPosition() == XAxisPosition.BOTTOM) {

                    offsetBottom += xLabelHeight;

                } else if (mXAxis.getPosition() == XAxisPosition.TOP) {

                    offsetTop += xLabelHeight;

                } else if (mXAxis.getPosition() == XAxisPosition.BOTH_SIDED) {

                    offsetBottom += xLabelHeight;
                    offsetTop += xLabelHeight;
                }
            }

            offsetTop += getExtraTopOffset();
            offsetRight += getExtraRightOffset();
            offsetBottom += getExtraBottomOffset();
            offsetLeft += getExtraLeftOffset();

            float minOffset = Utils.convertDpToPixel(mMinOffset);

            mViewPortHandler.restrainViewPort(
                    Math.max(minOffset, offsetLeft),
                    Math.max(minOffset, offsetTop),
                    Math.max(minOffset, offsetRight),
                    Math.max(minOffset, offsetBottom));

            if (mLogEnabled) {
                Log.i(LOG_TAG, "offsetLeft: " + offsetLeft + ", offsetTop: " + offsetTop
                        + ", offsetRight: " + offsetRight + ", offsetBottom: " + offsetBottom);
                Log.i(LOG_TAG, "Content: " + mViewPortHandler.getContentRect().toString());
            }
        }

        prepareOffsetMatrix();
        prepareValuePxMatrix();
    }
複製代碼

經過一系列的操做計算出Chart內容區域的offset,而後經過ViewPortHandler.restrainViewPort()重置Chart內容區域大小。 這是作的第一件事(從新計算Chart的內容大小)。而接下來的prepareOffsetMatrix和prepareValuePxMatrix則作了第二件事(計算數據和像素的縮放比例)。一樣整理一張圖來輔助理解這個過程:

Transformer數據與像素映射流程.png

在notifyDataSetChange的時候經過調用到Transformer的prepareMatrixXXX()方法設置好Transformer.Matrix的平移縮放比。而後在真正執行繪製操做的時候,再使用Transformer計算出實際的繪製座標區域。

Transform.使用Matrix完成 "value-touch-offset" 過程。也就是數據值到像素值的映射關係。

設置的屬性(什麼時候)起到什麼做用

經過閱讀源碼可知。在Renderer中具體執行繪製操做的時候。會根據咱們以前設置的屬性執行相關的操做。好比若是設置了dataSet.isDrawFilledEnabled爲true,則會執行drawLinearFill方法。在使用canvas.drawLines時會使用咱們經過的dataSet.setColors使用的顏色等等。具體的操做能夠根據須要深刻源碼瞭解。ps:下一次Draw生效

MP的架構設計是怎樣的

對我而言,評論一個第三方庫到底好很差的原則不徹底在於它的功能是否完美。而在於它的設計以及他的擴展到底好很差。做爲第三庫被應用的場景是多種多樣的,若是可以作到儘量的「適合」運用到項目中,並可以自由的給使用者進行擴展,這樣的設計和架構纔是真正最具備學習異議的。相信經過以上分析,對於這個問題應該有了屬於本身的看法了。

手勢控制

MP支持對拖拽,縮放,平移等操做。內部已經實現了具體的細節,並提供了相應的「開關」供使用者選擇。並提供了相應的接口回調具體的細節到外層,外層只需提供具體的回調接口便可。整理了下BarLineChartTouchListener類的onTouch方法流程以下:

ChartTouchListener流程圖

關於擴展

MP原本提供了許多功能和Api接口。總體的功能很是豐富和完成。可是大多數狀況下,實際需求須要咱們進一步的對MP進行擴展和瘋子。好在MP的可擴展性很是良好,咱們日常對MP擴展主要分爲三種方式:

  • MP自己提供一些擴展類以供一些較爲常見的需求。好比MarkerView、數據格式化等。好比財務圖表中,對應Y軸label和圖中的value均保留兩位精度。經過實現IValueFormatter接口,並設置給圖表便可。
  • 一種是對圖表或者組件進行擴展,項目中通常是基於MP圖表基礎上增長咱們想要的屬性設置和方法。好比UsYAxis在YAxis基礎上增長了mLongestLabel使得多張圖表的狀況下兩側可以以一個約定值對其
  • 二是對MP各類方法進行擴展重寫,常見的是對X/Y軸,以及各種Renderer進行方法重寫。好比財務圖表中,兩條線的value須要將較大值繪製在上方,較小值繪製在下方。所以對LineChartRenderer的drawValues方法進行重寫以便達到這種效果。

在實際的使用場景中,根據具體的業務邏輯選擇一到多種的擴展方式進行結合達到咱們的需求。可是總體均保持不動MP源碼的基礎上進行擴展和封裝。以便於之後兼任MP版本升級帶來的影響

相關文章
相關標籤/搜索