我们知道,MPAndroidChart中的LineChart是支持动态添加数据的,也就是说,如果我们需要实现动态的折线图,LineChart是完全可以满足需要的,下面的代码就可以实现这一效果:
public void addEntry(int number) {
//最开始的时候才添加lineDataSet(一个lineDataSet代表一条线)
if (mLineDataSet.getEntryCount() == 0) {
mLineData.addDataSet(mLineDataSet);
}
mLineChart.setData(mLineData);
Entry entry = new Entry(mLineDataSet.getEntryCount() - 1, number);
mLineData.addEntry(entry, 0);
//通知数据已经改变
mLineData.notifyDataChanged();
mLineChart.notifyDataSetChanged();
//设置在曲线中显示的最大数据量
mLineChart.setVisibleXRangeMaximum(30);
//移到某个位置
mLineChart.moveViewToX(mLineData.getEntryCount() - 5);
}
我们可以在TimerTask中循环调用该方法,就可以实现折线图的动态描点了,具体的效果可以见下图:
细心的朋友们可能会发现,当折线图刚开始描点时,点位并不是一个一个从左边或者右边“长”出来的,而是第一个点在坐标系的正中间,第二个点出来的时候,第一个点跑到了左边的Y轴上,第三个点出来的时候,第二个点又往左边移动一段距离,总结而言就是不管你设置了折线图中最多展示多少个点,初始的时候,点位与它们之间连接的折线都将是占满整个坐标系的,更进一步观察发现,图上已有的折线其实都是平均分配整个坐标系的空间的。
一般情况下,这并没有什么问题,但是如果我们的需求是要求初始化描点时,点是从左边Y轴一个一个“长”出来(如下图所示),整个折线是从左向右生长并慢慢填满整个坐标系的时候,我们应该怎么做呢。
查询MPAndroid的API发现,是没有提供对应的方法的。那么我们可以稍微深入一点,先去源码里面看看,LineChart是如何实现折线图的动态移动的。
在本文上方的addEntry方法中,有这么一句代码:
mLineChart.notifyDataSetChanged();
这个方法是不是很眼熟,没错,RecyclerView和ListView的Adapter都是用这个方法去通知数据集改变并刷新的。其实就是折线图动起来的关键所在,我们进入notifyDataSetChanged()看一看它做了点什么:
@Override
public void notifyDataSetChanged() {
if (mData == null) {
if (mLogEnabled)
Log.i(LOG_TAG, "Preparing... DATA NOT SET.");
return;
} else {
if (mLogEnabled)
Log.i(LOG_TAG, "Preparing...");
}
if (mRenderer != null)
mRenderer.initBuffers();
calcMinMax();
mAxisRendererLeft.computeAxis(mAxisLeft.mAxisMinimum, mAxisLeft.mAxisMaximum, mAxisLeft.isInverted());
mAxisRendererRight.computeAxis(mAxisRight.mAxisMinimum, mAxisRight.mAxisMaximum, mAxisRight.isInverted());
mXAxisRenderer.computeAxis(mXAxis.mAxisMinimum, mXAxis.mAxisMaximum, false);
if (mLegend != null)
mLegendRenderer.computeLegend(mData);
calculateOffsets();
}
方法中最开始的Log和渲染相关的代码我们先不去管,首先看一下calcMinMax()方法做了什么:
@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));
}
代码很简单,就是在三个坐标轴(X轴,左边Y轴,右边Y轴)上分别调用了calculate方法,跟踪进calculate方法:
/**
* Calculates the minimum / maximum and range values of the axis with the given
* minimum and maximum values from the chart data.
*
* @param dataMin the min value according to chart data
* @param dataMax the max value according to chart data
*/
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);
}
从注释可以看出,calculate方法单纯是用来计算坐标轴的最大最小值和值的范围的,那么具体代码就不再细说了。接下来返回到notifyDataSetChanged()方法中,calcMinMax()方法下面的关于坐标轴计算和图例相关的计算也不在我们关心的范围内,我们直接看最后一行代码,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();
}
嗯,代码比较长,但我们还是要坚持原则,去找我们需要的代码。方法里面进行了大量的偏移量计算,这仍然也不是我们关心的,那么跳过去,接下来就只剩下最后两行代码,prepareOffsetMatrix()方法和prepareValuePxMatrix()方法:
protected void prepareOffsetMatrix() {
mRightAxisTransformer.prepareMatrixOffset(mAxisRight.isInverted());
mLeftAxisTransformer.prepareMatrixOffset(mAxisLeft.isInverted());
}
prepareOffsetMatrix()方法中是为左边和右边的Y轴准备偏移量变换矩阵的,继续跟踪进去发现其实跟我们想要找的代码关系不大,这里不再继续深入。
protected void prepareValuePxMatrix() {
if (mLogEnabled)
Log.i(LOG_TAG, "Preparing Value-Px Matrix, xmin: " + mXAxis.mAxisMinimum + ", xmax: "
+ mXAxis.mAxisMaximum + ", xdelta: " + mXAxis.mAxisRange);
mRightAxisTransformer.prepareMatrixValuePx(mXAxis.mAxisMinimum,
mXAxis.mAxisRange,
mAxisRight.mAxisRange,
mAxisRight.mAxisMinimum);
mLeftAxisTransformer.prepareMatrixValuePx(mXAxis.mAxisMinimum,
mXAxis.mAxisRange,
mAxisLeft.mAxisRange,
mAxisLeft.mAxisMinimum);
}
接下来是prepareValuePxMatrix()方法,可以看到里面又调用了prepareMatrixValuePx()方法,看一下传递进去的参数,其中有一个mXAxis.mAxisRange,字面上就可以看出是X轴的取值范围,嗯到了这里就感觉有点眉目了,赶紧进去prepareValuePxMatrix()方法瞅瞅:
/**
* Prepares the matrix that transforms values to pixels. Calculates the
* scale factors from the charts size and offsets.
*
* @param xChartMin
* @param deltaX
* @param deltaY
* @param yChartMin
*/
public void prepareMatrixValuePx(float xChartMin, float deltaX, float deltaY, float yChartMin) {
float scaleX = (float) ((mViewPortHandler.contentWidth()) / deltaX);
float scaleY = (float) ((mViewPortHandler.contentHeight()) / deltaY);
if (Float.isInfinite(scaleX)) {
scaleX = 0;
}
if (Float.isInfinite(scaleY)) {
scaleY = 0;
}
// setup all matrices
mMatrixValueToPx.reset();
mMatrixValueToPx.postTranslate(-xChartMin, -yChartMin);
mMatrixValueToPx.postScale(scaleX, -scaleY);
}
可以看到,传递进来的实参mXAxis.mAxisRange用于计算了一个scaleX的浮点数,
float scaleX = (float) ((mViewPortHandler.contentWidth()) / deltaX);
这个scaleX最后又被传入了postScale()方法:
mMatrixValueToPx.postScale(scaleX, -scaleY);
跟踪进去发现最后调到了native方法,呃呃超出我们的范围了。那么接下来我们可以开始从这里进行一些尝试,比如首先试着去修改一下,初始化描点时,mXAxis.mAxisRange的值。
回顾一下上面的代码,可以发现mXAxis.mAxisRange的值是在calculate()方法中的最后一句代码进行修改的:
// calc actual range
this.mAxisRange = Math.abs(this.mAxisMaximum - this.mAxisMinimum);
接下来我们可以新建一个类,MyLineChart,继承LineChart,并写一个新的notifyDataSetChanged()方法,这里我取名为notifyDataSetChangedNew():
public void notifyDataSetChangedNew() {
if (mData == null) {
if (mLogEnabled)
Log.i(LOG_TAG, "Preparing... DATA NOT SET.");
return;
} else {
if (mLogEnabled)
Log.i(LOG_TAG, "Preparing...");
}
if (mRenderer != null)
mRenderer.initBuffers();
calcMinMax();
mXAxis.mAxisRange = 30;
mAxisRendererLeft.computeAxis(mAxisLeft.mAxisMinimum, mAxisLeft.mAxisMaximum, mAxisLeft.isInverted());
mAxisRendererRight.computeAxis(mAxisRight.mAxisMinimum, mAxisRight.mAxisMaximum, mAxisRight.isInverted());
mXAxisRenderer.computeAxis(mXAxis.mAxisMinimum, mXAxis.mAxisMaximum, false);
if (mLegend != null)
mLegendRenderer.computeLegend(mData);
calculateOffsets();
}
上面代码中的30是我们设置的折线图中包含的最大的点的数量。
然后我们修改一下文章一开头贴出来的addEntry方法:
public void addEntry(int number) {
//最开始的时候才添加lineDataSet(一个lineDataSet代表一条线)
if (mLineDataSet.getEntryCount() == 0) {
mLineData.addDataSet(mLineDataSet);
}
mLineChart.setData(mLineData);
Entry entry = new Entry(mLineDataSet.getEntryCount() - 1, number);
mLineData.addEntry(entry, 0);
//通知数据已经改变
mLineData.notifyDataChanged();
if (mLineDataSet.getEntryCount() <= 30) {
mLineChart.notifyDataSetChangedNew();
} else {
mLineChart.notifyDataSetChanged();
}
//设置在曲线中显示的最大数据量
mLineChart.setVisibleXRangeMaximum(30);
//移到某个位置
mLineChart.moveViewToX(mLineData.getEntryCount() - 5);
}
这里稍微解释一下修改的地方,即当折线图中的点的数量还未达到最大数量30时,调用我们新写的notifyDataSetChangedNew()方法,把mXAxis.mAxisRange的值强制设置为30,而当点的数量大于30后,调用原来的notifyDataSetChanged()方法。
最后跑一下代码,发现折线图初始化的时候,点是从左边开始描,并且折线是从左往右“生长”的了。
然后个人认为在查看框架源码的时候,我们需要focus我们想要的关键代码,而不要过于关注代码具体细节,不然很容易陷入其中不能自拔。
文章中可能存在一些对代码理解上的不足,欢迎各位大佬不吝指教,另外,如何实现从右到左初始化折线也是可以去研究一下的。