前言
MPAndroidChart 作为Android平台上最火的图表库,功能齐全,但是相对而言,文档确很少,公司项目中有图表的需求,这里也使用的是MPAndroidChart,有部分定制性的功能,特此记录。
BarChart
- 1.X、Y方向值的自定义
默认的支持显示数字,如果要自定义的话,需要使用ValueFormatter
public static class MyValueFormatter extends ValueFormatter {
private List<String> list;
public MyValueFormatter(List<String> list) {
this.list = list;
}
@Override
public String getFormattedValue(float value) {
if (list != null && list.size() > value) {
return list.get((int) value);
}
return super.getFormattedValue(value);
}
}
如何使用?
List<String> titles = new ArrayList<>();
for (int i = 0; i < personList.size(); i++) {
JSONObject object = personList.getJSONObject(i);
values.add(new BarEntry(i, object.getFloatValue("requestTime")));
titles.add(object.getString("realName"));
colors.add(allColors.get(new Random().nextInt(6)));
}
XAxis xAxis = barChartOvertimePerson.getXAxis();
xAxis.setValueFormatter(new MyValueFormatter(titles));
-
2.圆角
image.png
默认的BarChart
只支持直角,要实现圆角的需要修改源码,通过阅读源码,我们发现,绘制柱状图的方法在
com.github.mikephil.charting.renderer.BarChartRenderer
类下的drawDataSet
方法完成。
...省略...
c.drawRect(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2],
buffer.buffer[j + 3], mRenderPaint);
...省略...
所以首先,我们需要使用我们自定义的CustBarChartRenderer
继承于BarChartRenderer
public class CustBarChartRenderer extends BarChartRenderer {
private RectF mBarShadowRectBuffer = new RectF();
public CustBarChartRenderer(BarDataProvider chart, ChartAnimator animator, ViewPortHandler viewPortHandler) {
super(chart, animator, viewPortHandler);
}
}
其次,重写BarChartRenderer#drawDataSet
方法,然后粘贴原本方法的完整代码,添加圆角代码;
@Override
protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) {
Transformer trans = mChart.getTransformer(dataSet.getAxisDependency());
mBarBorderPaint.setColor(dataSet.getBarBorderColor());
mBarBorderPaint.setStrokeWidth(Utils.convertDpToPixel(dataSet.getBarBorderWidth()));
final boolean drawBorder = dataSet.getBarBorderWidth() > 0.f;
float phaseX = mAnimator.getPhaseX();
float phaseY = mAnimator.getPhaseY();
// draw the bar shadow before the values
if (mChart.isDrawBarShadowEnabled()) {
mShadowPaint.setColor(dataSet.getBarShadowColor());
BarData barData = mChart.getBarData();
final float barWidth = barData.getBarWidth();
final float barWidthHalf = barWidth / 2.0f;
float x;
for (int i = 0, count = Math.min((int) (Math.ceil((float) (dataSet.getEntryCount()) * phaseX)), dataSet.getEntryCount());
i < count;
i++) {
BarEntry e = dataSet.getEntryForIndex(i);
x = e.getX();
mBarShadowRectBuffer.left = x - barWidthHalf;
mBarShadowRectBuffer.right = x + barWidthHalf;
trans.rectValueToPixel(mBarShadowRectBuffer);
if (!mViewPortHandler.isInBoundsLeft(mBarShadowRectBuffer.right)) {
continue;
}
if (!mViewPortHandler.isInBoundsRight(mBarShadowRectBuffer.left)) {
break;
}
mBarShadowRectBuffer.top = mViewPortHandler.contentTop();
mBarShadowRectBuffer.bottom = mViewPortHandler.contentBottom();
c.drawRect(mBarShadowRectBuffer, mShadowPaint);
}
}
// initialize the buffer
BarBuffer buffer = mBarBuffers[index];
buffer.setPhases(phaseX, phaseY);
buffer.setDataSet(index);
buffer.setInverted(mChart.isInverted(dataSet.getAxisDependency()));
buffer.setBarWidth(mChart.getBarData().getBarWidth());
buffer.feed(dataSet);
trans.pointValuesToPixel(buffer.buffer);
final boolean isSingleColor = dataSet.getColors().size() == 1;
if (isSingleColor) {
mRenderPaint.setColor(dataSet.getColor());
}
for (int j = 0; j < buffer.size(); j += 4) {
if (!mViewPortHandler.isInBoundsLeft(buffer.buffer[j + 2])) {
continue;
}
if (!mViewPortHandler.isInBoundsRight(buffer.buffer[j])) {
break;
}
if (!isSingleColor) {
// Set the color for the currently drawn value. If the index
// is out of bounds, reuse colors.
mRenderPaint.setColor(dataSet.getColor(j / 4));
}
if (dataSet.getGradientColor() != null) {
GradientColor gradientColor = dataSet.getGradientColor();
mRenderPaint.setShader(
new LinearGradient(
buffer.buffer[j],
buffer.buffer[j + 3],
buffer.buffer[j],
buffer.buffer[j + 1],
gradientColor.getStartColor(),
gradientColor.getEndColor(),
android.graphics.Shader.TileMode.MIRROR));
}
if (dataSet.getGradientColors() != null) {
mRenderPaint.setShader(
new LinearGradient(
buffer.buffer[j],
buffer.buffer[j + 3],
buffer.buffer[j],
buffer.buffer[j + 1],
dataSet.getGradientColor(j / 4).getStartColor(),
dataSet.getGradientColor(j / 4).getEndColor(),
android.graphics.Shader.TileMode.MIRROR));
}
float left = buffer.buffer[j];
float top = buffer.buffer[j + 1];
float right = buffer.buffer[j + 2];
float bottom = buffer.buffer[j + 3];
float radius = (right - left) / 2;
//高度减去圆的半径
c.drawRect(left, top + radius, right, bottom, mRenderPaint);
//画圆
c.drawCircle(left + radius, top + radius, radius, mRenderPaint);
if (drawBorder) {
c.drawRect(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2],
buffer.buffer[j + 3], mBarBorderPaint);
}
}
}
原理也很简单,先绘制一个减去半径高度的矩形,然后在矩形的左右居中位置绘制一个圆
最后,让我们自定义的CustBarChartRenderer
生效
public class CustBarChart extends BarChart {
public CustBarChart(Context context) {
super(context);
}
public CustBarChart(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustBarChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void init() {
super.init();
mRenderer = new CustBarChartRenderer(this, mAnimator, mViewPortHandler);
}
}
-
3.矩形的宽度
当x方向的数据比较少时,矩形的宽度太宽了,这显然不符合产品大大的需要,我们还需要限制矩形的宽度
image.png
如何限制呢?在上一个问题中,我们找到了绘制矩形的地方,我们只需要对矩形的坐标重新进行计算,就可以达到限制宽度的作用
...省略...
float left = buffer.buffer[j];
float top = buffer.buffer[j + 1];
float right = buffer.buffer[j + 2];
float bottom = buffer.buffer[j + 3];
float radius = (right - left) / 2;
float max = 40;
if (radius > max) {
float xl = right - left - max;
//控制大小
left = left + xl / 2;
right = right - xl / 2;
radius = max / 2;
}
//高度减去圆的半径
c.drawRect(left, top + radius, right, bottom, mRenderPaint);
//画圆
c.drawCircle(left + radius, top + radius, radius, mRenderPaint);
...省略...
当然,这个最大值可以根据项目的实际情况做修改。
-
4.x方向文字过长
image.png
可以看到,当x方向文字过长时,文字之间会覆盖,作者也考虑到了这个问题,提供了一个方法
XAxis xAxis = barChart.getXAxis();
xAxis.setLabelRotationAngle(90);
效果如下
可惜的是,产品大大一般都不会满足于此,下图才是人家需要的。
image.png
可能有人说了,使用
\n
可以不,例如丈\n八\n一\n路
,实际上并没有什么用,有兴趣的同学可以试试。还是翻源码吧。通过翻源码,我们发现x方向的文字绘制是在
com.github.mikephil.charting.renderer.XAxisRenderer
类下drawLabel
方法中,还是同样的套路,我们重写这个方法。
思路还是通\n
来实现,只是原先的用的Canvas#drawText
方法不支持,我们使用StaticLayout
来替代
@Override
protected void drawLabel(Canvas c, String formattedLabel, float x, float y, MPPointF anchor, float angleDegrees) {
if (isMultilineText(formattedLabel)) {
TextPaint textPaint = new TextPaint(mAxisLabelPaint);
drawMultilineText(c, formattedLabel, x, y,
textPaint,
new FSize(c.getWidth(), c.getHeight()), new MPPointF(0f, 0f), 0);
} else {
Utils.drawXAxisValue(c, formattedLabel, x, y, mAxisLabelPaint, anchor, angleDegrees);
}
}
首先需要判断是否是多行文字,单行的话,走之前默认的方法,多行的话,调用drawMultilineText()
方法
private boolean isMultilineText(String text) {
if (TextUtils.isEmpty(text)) {
return false;
}
return text.contains("\n");
}
drawMultilineText()
private void drawMultilineText(Canvas c, String text,
float x, float y,
TextPaint paint,
FSize constrainedToSize,
MPPointF anchor, float angleDegrees) {
StaticLayout textLayout = new StaticLayout(
text, 0, text.length(),
paint,
(int) Math.max(Math.ceil(constrainedToSize.width), 1.f),
Layout.Alignment.ALIGN_NORMAL, 1.f, 0.f, false);
drawMultilineText(c, textLayout, x, y, paint, anchor, angleDegrees);
}
public void drawMultilineText(Canvas c, StaticLayout textLayout,
float x, float y,
TextPaint paint,
MPPointF anchor, float angleDegrees) {
float drawOffsetX = 0.f;
float drawOffsetY = 0.f;
float drawWidth;
float drawHeight;
final float lineHeight = paint.getFontMetrics(mFontMetricsBuffer);
drawWidth = textLayout.getWidth();
drawHeight = textLayout.getLineCount() * lineHeight;
// Android sometimes has pre-padding
drawOffsetX -= mDrawTextRectBuffer.left;
// Android does not snap the bounds to line boundaries,
// and draws from bottom to top.
// And we want to normalize it.
// drawOffsetY += drawHeight;
// To have a consistent point of reference, we always draw left-aligned
Paint.Align originalTextAlign = paint.getTextAlign();
paint.setTextAlign(Paint.Align.CENTER);
if (angleDegrees != 0.f) {
// Move the text drawing rect in a way that it always rotates around its center
// drawOffsetX -= drawWidth * 0.5f;
drawOffsetX += x + (mDrawTextRectBuffer.width() / 2.0f);
drawOffsetY -= drawHeight * 0.5f;
float translateX = x;
float translateY = y;
// Move the "outer" rect relative to the anchor, assuming its centered
if (anchor.x != 0.5f || anchor.y != 0.5f) {
final FSize rotatedSize = getSizeOfRotatedRectangleByDegrees(
drawWidth,
drawHeight,
angleDegrees);
translateX -= rotatedSize.width * (anchor.x - 0.5f);
translateY -= rotatedSize.height * (anchor.y - 0.5f);
FSize.recycleInstance(rotatedSize);
}
c.save();
c.translate(translateX, translateY);
c.rotate(angleDegrees);
c.translate(drawOffsetX, drawOffsetY);
textLayout.draw(c);
c.restore();
} else {
if (anchor.x != 0.f || anchor.y != 0.f) {
drawOffsetX -= drawWidth * anchor.x;
drawOffsetY -= drawHeight * anchor.y;
}
drawOffsetX += x;
drawOffsetY += y;
c.save();
c.translate(drawOffsetX, drawOffsetY);
textLayout.draw(c);
c.restore();
}
paint.setTextAlign(originalTextAlign);
}
/**
* Returns a recyclable FSize instance.
* Represents size of a rotated rectangle by degrees.
*
* @param rectangleWidth
* @param rectangleHeight
* @param degrees
* @return A Recyclable FSize instance
*/
public static FSize getSizeOfRotatedRectangleByDegrees(float rectangleWidth, float
rectangleHeight, float degrees) {
final float radians = degrees * ((float) Math.PI / 180.f);
return getSizeOfRotatedRectangleByRadians(rectangleWidth, rectangleHeight, radians);
}
该方法摘选自com.github.mikephil.charting.utils.Utils
,这里做了简单修改。
然后是使我们的CustXAxisRenderer
生效,还是在init
方法中
public class CustBarChart extends BarChart {
public CustBarChart(Context context) {
super(context);
}
public CustBarChart(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustBarChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void init() {
super.init();
mRenderer = new CustBarChartRenderer(this, mAnimator, mViewPortHandler);
mXAxisRenderer = new CustXAxisRenderer(mViewPortHandler, mXAxis, mLeftAxisTransformer);
}
}
然后是搭配ValueFormatter
使用
public static class MyValueFormatter1 extends ValueFormatter {
private List<String> list;
private boolean needMultLine;
public MyValueFormatter1(List<String> list, boolean needMultLine) {
this.list = list;
this.needMultLine = needMultLine;
}
public MyValueFormatter1(List<String> list) {
this.list = list;
}
@Override
public String getFormattedValue(float value) {
if (list != null && list.size() > value) {
if (needMultLine) {
String text = list.get((int) value);
if (!TextUtils.isEmpty(text) && text.length() > 0) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < text.length(); i++) {
builder.append(text.charAt(i)).append("\n");
}
return builder.toString();
}
return text;
}
return list.get((int) value);
}
return super.getFormattedValue(value);
}
}
XAxis xAxis = barChart.getXAxis();
xAxis.setValueFormatter(new CustBarChart.MyValueFormatter1(titles, true));
最后,还需要设置barChart.setExtraBottomOffset(50);
,需要注意的是这里的50
需要根据实际情况做修改,如需动态设置,这里提供个思路,drawMultilineText
方法中的drawHeight
即为每个多行文字的高度,然后需要计算最大值,然后在调用setExtraBottomOffset
即可。
PieChart
- 1.灰色背景
饼状图当数据的数值是0时,默认只有文字描述。但是产品大大的需求是这样
image.png
思路是这样,判断当前所有数据的数值都是0时,在数据后再添加一个100的数据,颜色为灰色。
需要重写setData
方法
public class CustPieChart extends PieChart {
public CustPieChart(Context context) {
super(context);
}
public CustPieChart(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustPieChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void init() {
super.init();
}
@Override
public void setData(PieData data) {
for (IDataSet<?> set : data.getDataSets()) {
set.setDrawValues(false);
set.setDrawIcons(false);
if (set instanceof PieDataSet) {
PieDataSet pieDataSet = (PieDataSet) set;
if (pieDataSet.getValues() != null && pieDataSet.getValues().size() > 0) {
boolean isEmpty = true;
for (PieEntry p : pieDataSet.getValues()) {
if (p.getValue() != 0) {
isEmpty = false;
}
}
if (isEmpty) {
pieDataSet.getValues().add(new PieEntry(100f, "empty"));
pieDataSet.getColors().add(Color.parseColor("#eeeeee"));
}
}
}
}
super.setData(data);
}
}
这样的话,指示栏中也会相应的多一条数据,我们需要排除这条数据,通过翻源码,我们发现指示栏的数据处理是在com.github.mikephil.charting.renderer.LegendRenderer
的computeLegend
方法,我们重写这个方法
public class MyLegendRenderer extends LegendRenderer {
public MyLegendRenderer(ViewPortHandler viewPortHandler, Legend legend) {
super(viewPortHandler, legend);
}
@Override
public void computeLegend(ChartData<?> data) {
if (!mLegend.isLegendCustom()) {
computedEntries.clear();
// loop for building up the colors and labels used in the legend
for (int i = 0; i < data.getDataSetCount(); i++) {
IDataSet dataSet = data.getDataSetByIndex(i);
List<Integer> clrs = dataSet.getColors();
int entryCount = dataSet.getEntryCount();
// if we have a barchart with stacked bars
if (dataSet instanceof IBarDataSet && ((IBarDataSet) dataSet).isStacked()) {
IBarDataSet bds = (IBarDataSet) dataSet;
String[] sLabels = bds.getStackLabels();
for (int j = 0; j < clrs.size() && j < bds.getStackSize(); j++) {
computedEntries.add(new LegendEntry(
sLabels[j % sLabels.length],
dataSet.getForm(),
dataSet.getFormSize(),
dataSet.getFormLineWidth(),
dataSet.getFormLineDashEffect(),
clrs.get(j)
));
}
if (bds.getLabel() != null) {
// add the legend description label
computedEntries.add(new LegendEntry(
dataSet.getLabel(),
Legend.LegendForm.NONE,
Float.NaN,
Float.NaN,
null,
ColorTemplate.COLOR_NONE
));
}
} else if (dataSet instanceof IPieDataSet) {
IPieDataSet pds = (IPieDataSet) dataSet;
for (int j = 0; j < clrs.size() && j < entryCount; j++) {
//排除我们添加的空数据
if (!"empty".equals(pds.getEntryForIndex(j).getLabel())) {
computedEntries.add(new LegendEntry(
pds.getEntryForIndex(j).getLabel(),
dataSet.getForm(),
dataSet.getFormSize(),
dataSet.getFormLineWidth(),
dataSet.getFormLineDashEffect(),
clrs.get(j)
));
}
}
if (pds.getLabel() != null) {
// add the legend description label
computedEntries.add(new LegendEntry(
dataSet.getLabel(),
Legend.LegendForm.NONE,
Float.NaN,
Float.NaN,
null,
ColorTemplate.COLOR_NONE
));
}
} else if (dataSet instanceof ICandleDataSet && ((ICandleDataSet) dataSet).getDecreasingColor() !=
ColorTemplate.COLOR_NONE) {
int decreasingColor = ((ICandleDataSet) dataSet).getDecreasingColor();
int increasingColor = ((ICandleDataSet) dataSet).getIncreasingColor();
computedEntries.add(new LegendEntry(
null,
dataSet.getForm(),
dataSet.getFormSize(),
dataSet.getFormLineWidth(),
dataSet.getFormLineDashEffect(),
decreasingColor
));
computedEntries.add(new LegendEntry(
dataSet.getLabel(),
dataSet.getForm(),
dataSet.getFormSize(),
dataSet.getFormLineWidth(),
dataSet.getFormLineDashEffect(),
increasingColor
));
} else { // all others
for (int j = 0; j < clrs.size() && j < entryCount; j++) {
String label;
// if multiple colors are set for a DataSet, group them
if (j < clrs.size() - 1 && j < entryCount - 1) {
label = null;
} else { // add label to the last entry
label = data.getDataSetByIndex(i).getLabel();
}
computedEntries.add(new LegendEntry(
label,
dataSet.getForm(),
dataSet.getFormSize(),
dataSet.getFormLineWidth(),
dataSet.getFormLineDashEffect(),
clrs.get(j)
));
}
}
}
if (mLegend.getExtraEntries() != null) {
Collections.addAll(computedEntries, mLegend.getExtraEntries());
}
mLegend.setEntries(computedEntries);
}
Typeface tf = mLegend.getTypeface();
if (tf != null)
mLegendLabelPaint.setTypeface(tf);
mLegendLabelPaint.setTextSize(mLegend.getTextSize());
mLegendLabelPaint.setColor(mLegend.getTextColor());
// calculate all dimensions of the mLegend
mLegend.calculateDimensions(mLegendLabelPaint, mViewPortHandler);
}
}
最后,使我们的MyLegendRenderer
生效
@Override
protected void init() {
super.init();
mLegendRenderer = new MyLegendRenderer(mViewPortHandler, mLegend);
}
网友评论