NanBox

给 Android 图表库 MPAndroidChart 的坐标换行

做统计图时,经常会有这样的需求:x 坐标需要换行显示,日和月或者是月和年需要分两行显示。但是找遍 MPAndroidChart 的各种教程,好像也没有相关的方法可以实现……

首先,MPAndroidChart 确实是没有提供更改坐标换行显示的方法。在看这篇文章之前,你可能已经尝试过实现一个 ValueFormatter,或者是 “\r\n” 这样的挣扎,发现并没有什么卵用。但就算真给你换行了,如何居中,如何设置两行不同字体也是问题。

咋办?看一下源码吧。看看设进去的数据是如何显示出来的。

以 LineChart 为例,我们是这样来设置坐标轴要显示的内容的:

1
2
3
LineData data = new LineData(xValues, dataSets);
mChart.setData(data);
mChart.invalidate();

跟着 setData 方法进源码,一路经过 Chart,BarLineChartBase,最终来到了 XAxisRenderer 这个类,我们的 xValues 被赋值给了它的 mXAxis。然后,好像就没有然后了……

不对,别忘了我们 setData 之后还调用了 invalidate 方法请求重绘,重绘会去调用 Chart 的 onDraw 方法。再看看继承 Chart 的 BarLineChartBase,它的 onDraw 方法里出现了这一句:

1
this.mXAxisRenderer.renderGridLines(canvas);

感觉离真相越来越近了。我们再跟着这个方法进来,看到里面调用了 drawLabels 方法,然后 drawLabels 又调用了 drawLabel 方法,最终我们来到了这里:

1
2
3
4
protected void drawLabel(Canvas c, String label, int xIndex, float x, float y, PointF anchor,float angleDegrees) {
String formattedLabel = mXAxis.getValueFormatter().getXValue(label, xIndex,mViewPortHandler);
Utils.drawXAxisValue(c, formattedLabel, x, y, mFirstLinePaint, anchor, angleDegrees);
}

最后进入 Utils.drawXAxisValue 方法,终于看到了这样的一句:

1
c.drawText(text, drawOffsetX, drawOffsetY, paint);

哎呦我去,终于找到了,藏这么深。

可以看到坐标轴是用 Canvas.drawText 显示出来的。了解一下你会发现,通常情况下, drawText 方法是不支持 “\n\r” 的。看来我们只要改 drawLabel 这个方法就可以了。由于源码几个类的耦合程度比较高,我并没有找到可以通过继承重写的方法实现,不得已只好改源代码了。

我修改后的代码是这样的:

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
protected void drawLabel(Canvas c, String label, int xIndex, float x, float y, PointF anchor, float angleDegrees) {
String formattedLabel = mXAxis.getValueFormatter().getXValue(label, xIndex, mViewPortHandler);
float labelHeight = mXAxis.getTextSize();
float labelInterval = 25f;
String[] labels = label.split(" ");
Paint mFirstLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mFirstLinePaint.setColor(Color.WHITE);
mFirstLinePaint.setTextAlign(Align.CENTER);
mFirstLinePaint.setTextSize(Utils.convertDpToPixel(15f));
mFirstLinePaint.setTypeface(mXAxis.getTypeface());
Paint mSecondLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mSecondLinePaint.setColor(0xFF9b9b9b);
mSecondLinePaint.setTextAlign(Align.CENTER);
mSecondLinePaint.setTextSize(Utils.convertDpToPixel(10f));
mSecondLinePaint.setTypeface(mXAxis.getTypeface());
if (labels.length > 1) {
Utils.drawXAxisValue(c, labels[0], x, y, mFirstLinePaint, anchor, angleDegrees);
Utils.drawXAxisValue(c, labels[1], x, y + labelHeight + labelInterval, mSecondLinePaint, anchor, angleDegrees);
} else {
Utils.drawXAxisValue(c, formattedLabel, x, y, mFirstLinePaint, anchor, angleDegrees);
}
}

我在要分行显示的字符串间加了个空格,在这里再用空格切割成两个字符串。我们可以分别给两行设置不同的格式,并且让他们都居中显示。最后绘制的时候,把第二行的 y 坐标改一下,在第一行的基础上加上一定的高度就可以了。妥妥的。

另外,那条黄色的高亮线也是改源码实现的。


2017.2.19 更新

MPAndroidChart 3.0 之后改变比较大,和这里相关的主要有两点:

  1. 取消了 LineData(List xVals, List dataSets) 这个构造方法,不再传x轴坐标数据,直接从 LineDataSet 坐标中获取。
  2. MPAndroidChart 底层绘制X轴坐标,是先格式化之后才执行 drawLabel 这个方法。

处理的思路跟之前是一致的,可以用 ValueFormatter 格式化一下X轴的数据(比如用空格将要分行显示的数据分开),然后修改源码 XAxisRenderer 里面的 drawLabel,类似下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected void drawLabel(Canvas c, String formattedLabel, float x, float y, MPPointF anchor, float angleDegrees) {
float labelHeight = mXAxis.getTextSize();
float labelInterval = 25f;
String[] labels = formattedLabel.split(" ");
Paint mFirstLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mFirstLinePaint.setColor(Color.WHITE);
mFirstLinePaint.setTextAlign(Align.CENTER);
mFirstLinePaint.setTextSize(Utils.convertDpToPixel(15f));
mFirstLinePaint.setTypeface(mXAxis.getTypeface());
Paint mSecondLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mSecondLinePaint.setColor(0xFF9b9b9b);
mSecondLinePaint.setTextAlign(Align.CENTER);
mSecondLinePaint.setTextSize(Utils.convertDpToPixel(10f));
mSecondLinePaint.setTypeface(mXAxis.getTypeface());
if (labels.length > 1) {
Utils.drawXAxisValue(c, labels[0], x, y, mFirstLinePaint, anchor, angleDegrees);
Utils.drawXAxisValue(c, labels[1], x, y + labelHeight + labelInterval, mSecondLinePaint, anchor, angleDegrees);
} else {
Utils.drawXAxisValue(c, formattedLabel, x, y, mFirstLinePaint, anchor, angleDegrees);
}
}

妥妥的。


2017.3.17 更新

补充一下高亮线的修改方法吧。

如果用的是不规则的高亮线(像图片里面上下有个球的),需要用自己的图片资源来替换。为了适配,建议使用高度可以拉伸的点九图。

我们先找到接口 ILineScatterCandleRadarDataSet,增加一个获取 Bitmap 的方法:

1
2
3
4
/**
* @return 获取高亮线图片 bitmap
*/
Bitmap getHighLightBitmap();

再找到 LineScatterCandleRadarDataSet,增加这两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected Bitmap mHighlightBitmap = null;
/**
* 设置高亮线图片
*
* @param bitmap 高亮线图片 bitmap
*/
public void setHighlightBitmap(Bitmap bitmap) {
this.mHighlightBitmap = bitmap;
}
/**
* @return 获取高亮线图片 bitmap
*/
public Bitmap getHighLightBitmap() {
return this.mHighlightBitmap;
}

最后找到设置高亮线的地方,它在 LineScatterCandleRadarRenderer 的 drawHighlightLines 方法里,可以把它修改成这个样子:

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
/**
* Draws vertical & horizontal highlight-lines if enabled.
*
* @param c
* @param x x-position of the highlight line intersection
* @param y y-position of the highlight line intersection
* @param set the currently drawn dataset
*/
protected void drawHighlightLines(Canvas c, float x, float y, ILineScatterCandleRadarDataSet set) {
// set color and stroke-width
mHighlightPaint.setColor(set.getHighLightColor());
mHighlightPaint.setStrokeWidth(set.getHighlightLineWidth());
// draw highlighted lines (if enabled)
mHighlightPaint.setPathEffect(set.getDashPathEffectHighlight());
//优先使用高亮线图片显示,没有则使用默认样式
if (set.getHighLightBitmap() != null) {
if (set.isVerticalHighlightIndicatorEnabled()) {
Bitmap highLightBitmap = set.getHighLightBitmap();
NinePatch ninePatch = new NinePatch(highLightBitmap,
highLightBitmap.getNinePatchChunk(), null);
int highLightWidth = (int) Utils.convertDpToPixel(8.0F);
RectF rectF = new RectF(x - (float) (highLightWidth / 2),
this.mViewPortHandler.contentTop(),
x + (float) (highLightWidth / 2),
this.mViewPortHandler.contentBottom());
ninePatch.draw(c, rectF);
}
} else {
// draw vertical highlight lines
if (set.isVerticalHighlightIndicatorEnabled()) {
// create vertical path
mHighlightLinePath.reset();
mHighlightLinePath.moveTo(x, mViewPortHandler.contentTop());
mHighlightLinePath.lineTo(x, mViewPortHandler.contentBottom());
c.drawPath(mHighlightLinePath, mHighlightPaint);
}
// draw horizontal highlight lines
if (set.isHorizontalHighlightIndicatorEnabled()) {
// create horizontal path
mHighlightLinePath.reset();
mHighlightLinePath.moveTo(mViewPortHandler.contentLeft(), y);
mHighlightLinePath.lineTo(mViewPortHandler.contentRight(), y);
c.drawPath(mHighlightLinePath, mHighlightPaint);
}
}
}

上面代码基于目前最新的 MPAndroidChart 3.0.1 ,旧版在细节上会有不同,但实现思路是一致的。

妥妥的。