索引
基础使用(静态图表)
定义
绘制一张图,包含线形图,饼图,柱状图等。能够设置各种配置,如颜色,字体大小,轴样式,数据格式化等等。
使用流程
可以通过继承和组合的方式选择具体的图表类型进行使用。如果将图表抽象成一个View来看,整个流程会比较清晰。大致分为以下几个步骤
- 基础配置:在初始化阶段做一些类似touch、drag、sacle属性的开关控制,X.Y轴、Legend、Description等的初始化设置(具体方法需看源码)
- 数据处理:将接口数据转换为图表需要的xxxEntry对象。然后进一步将其转换为xxxDataSet对象,并为其设置样式,最后合并成相应图表需要的xxxData。ChartData 与 DataSet的对应关系为 一对多 。DataSet 与 Entry的对应关系同样为 一对多
- 图表绘制:为Chart设置ChartData,使其重绘
如果对MP的Api使用有一定的了解后,通过以上的步骤就能轻松的实现一张图的绘制。
代码示例:
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做出一张图表。但是如果可能我们的数据不是一成不变的。可能随着时间推移,数据量增大。或者随着手势滑动,需要加载新的数据。我们可以通过两种方式动态更新数据(仅仅是据我所知):
- 使用MP自带的Api进行单个Entry的增删或者单个DataSet的增删
- 直接全量重置setData
说到底就是就是让Chart重新绘制了一次。具体怎样根据需求增加/更新数据才是重点。对于Chart来说,仅仅是重绘操作。
图表设置
关于图表具体的配置选项主要可以分为三类:
- 针对图表整体的通用和特殊设置
- 针对于Axis(轴)、Legend、LimitLine、Description等组件配置
- 针对数据显示的配置,xxDataSet/ChartDat
了解相关具体的配置需要深入其中查看详细的源码。而对Api的源码有一定的了解后,会为后面对MP的扩展打好基础。因为图表最终的绘制过程,都需要结合各种配置条件进行相应的绘制。关于MP中常用的属性设置和方式做了一份整理,详细情况可以了解这里
庖丁解牛
知道了如何使用MP,以及大致结构后。接下来便是进一步分析MP的绘制l流程。在次之前,我们先思考几个问题,然后带着这几个问题继续往下研:
- MP的架构设计是怎样的
- 设置的属性(何时)起到什么作用
- 数据是通过怎样的方式映射为视图View的
首先我们不要被“图表”这个词误导,说到底也就是一个View视图。Chart本身只是一个ViewGroup。只不过是由许多部分一一组成的。粗略的画了张原型图:

从原型图来看,Chart本身是个大容器或者说是一个组合体。大致由组件和内容两部分组成。组件主要包括XY轴、Legend、限制线等。组件可以单独进行设置。内容主要包括图表本身和数据的渲染。Chart包含一个或者多个组件,Chart会在适当的时机(实际计算/绘制的时候)通知组件做相应的操作。从前面的代码使用示例中也可以看出这一点。
其次要明确一个概念,Chart本身“不做任何计算和绘制的操作”。这里之所以用双引号的原因是因为在面向对象的思想上Chart只做事件执行的分发者,具体的数据计算、数据与像素位置转换、内容绘制等操作都是由另外的对象执行(对应MP中的各种Renderer)。有了以上的概念以后,来分析从setData到Chart到呈现到视图上之间的整个过程,直接上图:

图中的BarLineChartBase是Chart的直接实现类。看图可能比较懵逼,我大致的梳理成几个流程:
- Chart调起自身的notifyDataSetChange从而使得Chart知道数据发生了变化
- 通知组件重新执行计算操作。主要是XY轴组件计算起极值和区间。
- 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()做了两件事:
- 重新计算Chart的内容大小
- 并计算好数据和像素的缩放比例
说到这里不得不提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则做了第二件事(计算数据和像素的缩放比例)。同样整理一张图来辅助理解这个过程:

在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方法流程如下:

关于扩展
MP本来提供了许多功能和Api接口。整体的功能非常丰富和完成。但是大多数情况下,实际需求需要我们进一步的对MP进行扩展和疯子。好在MP的可扩展性非常良好,我们平常对MP扩展主要分为三种方式:
- MP本身提供一些扩展类以供一些较为常见的需求。比如MarkerView、数据格式化等。比如财务图表中,对应Y轴label和图中的value均保留两位精度。通过实现IValueFormatter接口,并设置给图表即可。
- 一种是对图表或者组件进行扩展,项目中一般是基于MP图表基础上增加我们想要的属性设置和方法。比如UsYAxis在YAxis基础上增加了mLongestLabel使得多张图表的情况下两侧能够以一个约定值对其
- 二是对MP各种方法进行扩展重写,常见的是对X/Y轴,以及各类Renderer进行方法重写。比如财务图表中,两条线的value需要将较大值绘制在上方,较小值绘制在下方。因此对LineChartRenderer的drawValues方法进行重写以便达到这种效果。
在实际的使用场景中,根据具体的业务逻辑选择一到多种的扩展方式进行结合达到我们的需求。但是整体均保持不动MP源码的基础上进行扩展和封装。以便于以后兼任MP版本升级带来的影响