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版本升级带来的影响