MPAndroidChart绘制浅析

前言

一直在使用MPAndroidChart但对其内部机制却没有做多少了解,自己之前还修改过MPAndroidChart的源码,某次面试被问到,MPAndroidChart是怎样进行绘制的,瞬间一脸懵逼,回答了个大概,但是被看出其实不是很了解。算亡羊补牢吧,今天抽了点时间看了MPAndroidChart 3.0的源码部分。
直接进入正题吧。

Chart基类

这边顺便讲解一下Chart这个类,它是所有图表类的抽象基类,继承自ViewGroup,实现了图表接口ChartInterface(这个接口用来实现图表的大小,边界,和范围的获取。),Chart里面存放了一些公共的配置和一些共有的抽象方法,数据等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
/**
* 这个类是图表类的基类,继承自ViewGroup,它可以让图表像View一样被使用。
* @author Philipp Jahoda
*/
@SuppressLint("NewApi")
public abstract class Chart<T extends ChartData<? extends IDataSet<? extends Entry>>> extends
ViewGroup
implements ChartInterface {
public static final String LOG_TAG = "MPAndroidChart";
/**
* 声明log是否开启(调试用)
*/
protected boolean mLogEnabled = false;
/**
* 图表数据
*/
protected T mData = null;
/**
* 触摸区域高亮
*/
protected boolean mHighLightPerTapEnabled = true;
/**
* 触摸事件完成后是否继续滚动(可以试试pieChart的旋转功能)
*/
private boolean mDragDecelerationEnabled = true;
/**
* 这个就是滚动的减慢速度,算摩擦系数,0就直接停了,1会一直转,所以他回自动变成0.999,
*/
private float mDragDecelerationFrictionCoef = 0.9f;
/**
* 数据格式化
*/
protected ValueFormatter mDefaultFormatter;
/**
* 图表描述画笔
*/
protected Paint mDescPaint;
/**
* 这个画笔是用来画无数据的情况
*/
protected Paint mInfoPaint;
/**
* 描述描述!!
*/
protected String mDescription = "Description";
/**
* X轴的label可以看折现图的横轴label
*/
protected XAxis mXAxis;
/**
* 手势开关
*/
protected boolean mTouchEnabled = true;
/**
* Legend是一个图例描述,里面是这个图例,位置等其他配置的信息。
*/
protected Legend mLegend;
/**
* 数据选中监听
*/
protected OnChartValueSelectedListener mSelectionListener;
/**
* 图表点击监听
*/
protected ChartTouchListener mChartTouchListener;
/**
* 空数据文本
*/
private String mNoDataText = "No chart data available.";
/**
* 手势监听
*/
private OnChartGestureListener mGestureListener;
/**
* 无数据描述
*/
private String mNoDataTextDescription;
/**
* 这个是图例绘制类
*/
protected LegendRenderer mLegendRenderer;
/**
* 数据绘制类基类实现
*/
protected DataRenderer mRenderer;
/**
* 区域高亮辅助基类。用来计算高亮区域并返回
*/
protected ChartHighlighter mHighlighter;
/**
* 边界约束管理
*/
protected ViewPortHandler mViewPortHandler;
/**
* 动画
*/
protected ChartAnimator mAnimator;
/**
* 边距
*/
private float mExtraTopOffset = 0.f,
mExtraRightOffset = 0.f,
mExtraBottomOffset = 0.f,
mExtraLeftOffset = 0.f;
/**
* 默认构造
*/
public Chart(Context context) {
super(context);
init();
}
/**
* constructor for initialization in xml
*/
public Chart(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* even more awesome constructor
*/
public Chart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
/**
* initialize all paints and stuff
* 初始化函数,负责一些对象的初始化
*/
protected void init() {
...略
}
/**
* 设置新数据,在这里有个notify,在数据添加完之后刷新图表
*/
public void setData(T data) {
// let the chart know there is new data
notifyDataSetChanged();
}
/**
* 清空所有数据并刷新
*/
public void clear() {
mData = null;
mIndicesToHighlight = null;
invalidate();
}
/**
* 上面是直接置空。可能是想回收,这里是清除值后刷新
*/
public void clearValues() {
mData.clearValues();
invalidate();
}
/**
* 判断data是否为空
*/
public boolean isEmpty() {
if (mData == null)
return true;
else {
if (mData.getYValCount() <= 0)
return true;
else
return false;
}
}
/**
* 这个方法可以让图表知道自己掌握的数据,并显示出来。
* 需要重新进行计算
*/
public abstract void notifyDataSetChanged();
/**
* 边距计算
*/
protected abstract void calculateOffsets();
/**
* 最大最小y值计算抽象方法
*/
protected abstract void calcMinMax();
/**
* 计算单位
*/
protected void calculateFormatter(float min, float max) {
...略
}
/**
* 边距计算
*/
private boolean mOffsetsCalculated = false;
protected Paint mDrawPaint;
/**
* 主要的绘制方法
*/
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
...略
}
/**
* 描述位置
*/
private PointF mDescriptionPosition;
/**
* 绘制描述
*/
protected void drawDescription(Canvas c) {
...略
}
/**高亮模块 支持点击高亮等各种效果。这边处理绘制还有计算*/
..略
/**下面是MarkView */
..略
/** 下面是动画处理 */
..略
/** 动画开放方法 */
public void animateXY(int durationMillisX, int durationMillisY, EasingFunction easingX,
EasingFunction easingY) {
mAnimator.animateXY(durationMillisX, durationMillisY, easingX, easingY);
}
public void animateX(int durationMillis, EasingFunction easing) {
mAnimator.animateX(durationMillis, easing);
}
public void animateY(int durationMillis, EasingFunction easing) {
mAnimator.animateY(durationMillis, easing);
}
/** 动画开放配置 */
public void animateXY(int durationMillisX, int durationMillisY, Easing.EasingOption easingX,
Easing.EasingOption easingY) {
mAnimator.animateXY(durationMillisX, durationMillisY, easingX, easingY);
}
public void animateX(int durationMillis, Easing.EasingOption easing) {
mAnimator.animateX(durationMillis, easing);
}
public void animateY(int durationMillis, Easing.EasingOption easing) {
mAnimator.animateY(durationMillis, easing);
}
public void animateX(int durationMillis) {
mAnimator.animateX(durationMillis);
}
public void animateXY(int durationMillisX, int durationMillisY) {
mAnimator.animateXY(durationMillisX, durationMillisY);
}
/**在往下就是各种配置了 */
略略略
}

它是所有图表的基类,里面是一些基础的方法,包括数据,高亮,动画,描述,空数据等公用方法的实现和抽象。保存当前图表信息等…
继承结构如下

  1. Chart(图表类基类)
    1. BarLineChartBase(柱状图折线图抽象类)
      1. BubbleChart(气泡图)
      2. CandleStickChart(烛状图)
      3. CombinedChart(复合图表)
      4. BarChart(柱状图)
      5. LineChart(折线图)
      6. ScatterChart(散点图)
    2. PieRadarChartBase(饼状图雷达图抽象类)
      1. PieChart(饼状图)
      2. RadarChart(雷达图)

根据不同的图表做了不同的实现,比如说BarLineChartBase都有一个共同的属性是XY轴,在onDraw方法中对XY轴做了绘制,BarLineChartBase还支持缩放操作。PieRadarBase是没有XY轴且不支持缩放操作,但支持旋转。所以将这两个图单独抽象了一层。
onDraw算是共有的主要方法。因为数据绘制都是在onDraw方法中的canvas上面。

下面来讲解一下MPAndroidChart的绘制过程

MPAndroidChart绘制过程

所有的绘制都做了抽象在这边基本能重用的方法就做一层抽象。
我们来看一下最底层的抽象Renderer类
Chart的绘制是经由Renderer类之手,看一下Renderer类的实现。

Render

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* Abstract baseclass of all Renderers.
*
* @author Philipp Jahoda
*/
public abstract class Renderer {
/**
* 这个变量用来存放绘制区域还有偏移量等设置
*/
protected ViewPortHandler mViewPortHandler;
/**
* X轴需要绘制的最小值
*/
protected int mMinX = 0;
/**
* X轴需要绘制的最大值
*/
protected int mMaxX = 0;
/**
* 这边通过构造传入ViewPortHandler
*/
public Renderer(ViewPortHandler viewPortHandler) {
this.mViewPortHandler = viewPortHandler;
}
/**
* 这个方法用来计算当前的值是否在X的最小和最大之间
*/
protected boolean fitsBounds(float val, float min, float max) {
if (val < min || val > max)
return false;
else
return true;
}
/**
* 这个方法用来计算当前可以显示的X的大小,
*/
public void calcXBounds(BarLineScatterCandleBubbleDataProvider dataProvider, int xAxisModulus) {
int low = dataProvider.getLowestVisibleXIndex();
int high = dataProvider.getHighestVisibleXIndex();
int subLow = (low % xAxisModulus == 0) ? xAxisModulus : 0;
mMinX = Math.max((low / xAxisModulus) * (xAxisModulus) - subLow, 0);
mMaxX = Math.min((high / xAxisModulus) * (xAxisModulus) + xAxisModulus, (int) dataProvider.getXChartMax());
}
}

这个类里面的方法用来确定当前视图可显示的大小,所有的Render类都继承自它。包括AxisRenderer(轴和轴值绘制),LegendRender(图例绘制),DataRender(图表图形绘制)。
AxisRenderer,和LegendRender的实现都大同小异,DataRender属于图表绘制的抽象,因为图表的样式比较多,它扩展了一些可以供图表使用的方法,接下来主要拿DataRender来讲。

DataRender

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/**
* 这个类是Renderer的子类,用来提供一些抽象的绘制方法。
*/
public abstract class DataRenderer extends Renderer {
/**
* 这个对象是用来设置图表动画的
*/
protected ChartAnimator mAnimator;
/**
* 初始化图表item绘制的画笔
*/
protected Paint mRenderPaint;
/**
* 绘制高亮区域
*/
protected Paint mHighlightPaint;
/**
* 这个没见用到
*/
protected Paint mDrawPaint;
/**
* 这个用来绘制图表的文本信息
*/
protected Paint mValuePaint;
/**
* 构造函数传入了动画对象ChartAnimator,控制绘制区域和偏移量的对象ViewPortHandler,里面做的是变量初始化操作
*/
public DataRenderer(ChartAnimator animator, ViewPortHandler viewPortHandler) {
super(viewPortHandler);
略..
}
/**
* 这个方法返回了文本绘制的画笔对象
*/
public Paint getPaintValues() {
return mValuePaint;
}
/**
* 高亮画笔
*/
public Paint getPaintHighlight() {
return mHighlightPaint;
}
/**
* 绘制画笔
*/
public Paint getPaintRender() {
return mRenderPaint;
}
/**
* Applies the required styling (provided by the DataSet) to the value-paint
* object.
*
* @param set
*/
protected void applyValueTextStyle(IDataSet set) {
mValuePaint.setTypeface(set.getValueTypeface());
mValuePaint.setTextSize(set.getValueTextSize());
}
/**
* 初始化Buffers,buffer是用来进行尺寸变换的一个类,他和transformer类配合生成实际的尺寸
*/
public abstract void initBuffers();
/**
* 数据绘制抽象类
*/
public abstract void drawData(Canvas c);
/**
*数值绘制抽象类
*/
public abstract void drawValues(Canvas c);
/**
* 数值绘制
*/
public void drawValue(Canvas c, ValueFormatter formatter, float value, Entry entry, int dataSetIndex, float x, float y, int color) {
mValuePaint.setColor(color);
c.drawText(formatter.getFormattedValue(value, entry, dataSetIndex, mViewPortHandler), x, y, mValuePaint);
}
/**
* 这个是为LineChart 或者PieChart等设计的附加绘制。因为lineChart需要为每个点进行绘制,PieChart可能需要绘制中间圆形等。
* @param c
*/
public abstract void drawExtras(Canvas c);
/**
* 绘制高亮数据
*/
public abstract void drawHighlighted(Canvas c, Highlight[] indices);
}

DataRender里面的方法大致如下

  1. 图表的绘制抽象方法 (drawData)
  2. 数值绘制的抽象方法 (drawValues)
  3. 数值绘制方法(drawValue)
  4. 图表高亮抽象方法(drawHighlighted)
  5. 动画对象和画笔对象的初始化(构造函数)

看一下BarChartRender的实现

BarChartRender


public class BarChartRenderer extends DataRenderer {
    /**
     * BarDataProvider这个类中存放了所有的barData,还有一些类似于阴影,数值位置,高亮箭头等。
     */
    protected BarDataProvider mChart;

    /** the rect object that is used for drawing the bars
     *  这个是用来设置每条bar的大小。主要是用做高亮绘制
     */
    protected RectF mBarRect = new RectF();
    /**
     * 声明buffer的数组
     */
    protected BarBuffer[] mBarBuffers;
    /**
     * 阴影画笔
     */
    protected Paint mShadowPaint;
    /**
     * 边框画笔
     */
    protected Paint mBarBorderPaint;

    /**
     * 构造函数传入了BarDataProvider,动画类,显示位置控制类,初始化了绘制所需的画笔
     */
    public BarChartRenderer(BarDataProvider chart, ChartAnimator animator,
            ViewPortHandler viewPortHandler) {
        super(animator, viewPortHandler);
        this.mChart = chart;

        mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mHighlightPaint.setStyle(Paint.Style.FILL);
        mHighlightPaint.setColor(Color.rgb(0, 0, 0));
        // set alpha after color
        mHighlightPaint.setAlpha(120);

        mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mShadowPaint.setStyle(Paint.Style.FILL);

        mBarBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBarBorderPaint.setStyle(Paint.Style.STROKE);
    }

    /**
     * 初始化Buffer
     */
    @Override
    public void initBuffers() {

        BarData barData = mChart.getBarData();
        mBarBuffers = new BarBuffer[barData.getDataSetCount()];

        for (int i = 0; i < mBarBuffers.length; i++) {
            IBarDataSet set = barData.getDataSetByIndex(i);
            mBarBuffers[i] = new BarBuffer(set.getEntryCount() * 4 * (set.isStacked() ? set.getStackSize() : 1),
                    barData.getGroupSpace(),
                    barData.getDataSetCount(), set.isStacked());
        }
    }

    /**
     * 绘制图表
     * @param c
     */
    @Override
    public void drawData(Canvas c) {

        BarData barData = mChart.getBarData();

        for (int i = 0; i < barData.getDataSetCount(); i++) {

            IBarDataSet set = barData.getDataSetByIndex(i);

            if (set.isVisible() && set.getEntryCount() > 0) {
                drawDataSet(c, set, i);
            }
        }
    }

    /**
     * 根据每个dataset绘制图表
     */
    protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) {
       ...略
    }

    /**
     * 准备高亮的区域
     */
    protected void prepareBarHighlight(float x, float y1, float y2, float barspaceHalf,
            Transformer trans) {

        float barWidth = 0.5f;

        float left = x - barWidth + barspaceHalf;
        float right = x + barWidth - barspaceHalf;
        float top = y1;
        float bottom = y2;

        mBarRect.set(left, top, right, bottom);

        trans.rectValueToPixel(mBarRect, mAnimator.getPhaseY());
    }

    /**
     * 绘制数值  一系列的计算
     */
    @Override
    public void drawValues(Canvas c) {
         略...     }

    /**
     * 绘制高亮  一系列的计算
     */
    @Override
    public void drawHighlighted(Canvas c, Highlight[] indices) {
            略...
    }

    public float[] getTransformedValues(Transformer trans, IBarDataSet data,
            int dataSetIndex) {
        return trans.generateTransformedValuesBarChart(data, dataSetIndex,
                mChart.getBarData(),
                mAnimator.getPhaseY());
    }

    /**
     * 检查是否为可显示的值
     * @return
     */
    protected boolean passesCheck() {
        return mChart.getBarData().getYValCount() < mChart.getMaxVisibleCount()
                * mViewPortHandler.getScaleX();
    }

    /**
     * 绘制其他
     * @param c
     */
    @Override
    public void drawExtras(Canvas c) { }
}

可以看到里面就是图表项和文本绘制的具体实现了,主要方法是drawvalues方法,大致就是一些通过一些方法计算每条bar的值,然后进行绘制。

总结

在这边简单讲解一下设计的方法。因为所有的Chart都继承了ViewGroup,实现了View的onMeasure,onDraw,onLayout,onSizeChanged方法,所以它是可以像自定义控件一样来使用。
View的绘制都再Render中实现,不同图表实现了不同的Render,继承结构大概如下:

  1. Render基类
    1. AxisRender(轴绘制)
    2. DataRender(图表绘制抽象类)
      1. CombinedChartRender(复合图表绘制,这个类是3.0版本添加的,可以展示折线图,柱状图,散点图等混合)
      2. BubbleChartRenderer(气泡图绘制)
      3. BarChartRender(柱状图绘制)
      4. LineScatterCandleRadarRenderer(折线图,散点图,烛状图,雷达图抽象类)
        1. LineRadarRenderer(折线图,雷达图抽象)
          1. LineChartRender(折线图绘制类)
          2. RadarChartRender(雷达图绘制类)
        2. ScatterChartRender(散点图绘制)
        3. CandleStickChartRenderer(烛状图绘制)
    3. LegendRender(图例绘制)

在不同的图表构造中初始化不同的将mRender对象初始化成不同的图表的Render对象,这里传入的参数有

  1. 不同图表的DataProvider(DataProvider是一个接口,实现了获取Y轴方向(左或右),和获取数据的方法)。
  2. ChartAnimator这是一个动画类,执行动画效果
  3. ViewPortHandler 图表信息类,包括边距,大小,转换等级(缩放)
    之后这些Render类就根据自己的实现在canvas上面绘制东西了。

其他补充

由于很好奇它的点击事件是怎么实现的,这边也看了一下它的点击事件。
每个图表写了自己的TouchListener
在构造中需要传入的参数有

  1. 图表大小的矩阵(用来计算缩放等级,还有当前点击事件位置)
  2. 图表对象

之后根据Touch事件判断相应的手势或者点击,触摸事件作出反应(点击,手势缩放,移动等)。调用View的postInvalidate 方法通知刷新。

By Xiaolong,每一天都值得被认真对待!