前言
MPAndroidChart 作為Android平臺(tái)上最火的圖表庫,功能齊全,但是相對(duì)而言宏所,文檔確很少肚邢,公司項(xiàng)目中有圖表的需求壹堰,這里也使用的是MPAndroidChart拭卿,有部分定制性的功能,特此記錄贱纠。
BarChart
- 1.X峻厚、Y方向值的自定義
默認(rèn)的支持顯示數(shù)字,如果要自定義的話谆焊,需要使用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.圓角
默認(rèn)的BarChart
只支持直角,要實(shí)現(xiàn)圓角的需要修改源碼辖试,通過閱讀源碼辜王,我們發(fā)現(xiàn),繪制柱狀圖的方法在
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);
}
}
}
原理也很簡單,先繪制一個(gè)減去半徑高度的矩形怒见,然后在矩形的左右居中位置繪制一個(gè)圓
最后俗慈,讓我們自定義的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.矩形的寬度
當(dāng)x方向的數(shù)據(jù)比較少時(shí),矩形的寬度太寬了遣耍,這顯然不符合產(chǎn)品大大的需要闺阱,我們還需要限制矩形的寬度
如何限制呢?在上一個(gè)問題中舵变,我們找到了繪制矩形的地方酣溃,我們只需要對(duì)矩形的坐標(biāo)重新進(jìn)行計(jì)算,就可以達(dá)到限制寬度的作用
...省略...
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);
...省略...
當(dāng)然纪隙,這個(gè)最大值可以根據(jù)項(xiàng)目的實(shí)際情況做修改赊豌。
-
4.x方向文字過長
可以看到,當(dāng)x方向文字過長時(shí)绵咱,文字之間會(huì)覆蓋碘饼,作者也考慮到了這個(gè)問題,提供了一個(gè)方法
XAxis xAxis = barChart.getXAxis();
xAxis.setLabelRotationAngle(90);
效果如下
可惜的是悲伶,產(chǎn)品大大一般都不會(huì)滿足于此艾恼,下圖才是人家需要的。
可能有人說了麸锉,使用
\n
可以不钠绍,例如丈\n八\n一\n路
湃交,實(shí)際上并沒有什么用溯祸,有興趣的同學(xué)可以試試谨湘。還是翻源碼吧稠曼。通過翻源碼,我們發(fā)現(xiàn)x方向的文字繪制是在
com.github.mikephil.charting.renderer.XAxisRenderer
類下drawLabel
方法中磷脯,還是同樣的套路蛾找,我們重寫這個(gè)方法。
思路還是通\n
來實(shí)現(xià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);
}
}
首先需要判斷是否是多行文字,單行的話架曹,走之前默認(rèn)的方法,多行的話闹瞧,調(diào)用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));
最后万牺,還需要設(shè)置barChart.setExtraBottomOffset(50);
,需要注意的是這里的50
需要根據(jù)實(shí)際情況做修改洽腺,如需動(dòng)態(tài)設(shè)置脚粟,這里提供個(gè)思路,drawMultilineText
方法中的drawHeight
即為每個(gè)多行文字的高度蘸朋,然后需要計(jì)算最大值核无,然后在調(diào)用setExtraBottomOffset
即可。
PieChart
- 1.灰色背景
餅狀圖當(dāng)數(shù)據(jù)的數(shù)值是0時(shí)藕坯,默認(rèn)只有文字描述团南。但是產(chǎn)品大大的需求是這樣
思路是這樣,判斷當(dāng)前所有數(shù)據(jù)的數(shù)值都是0時(shí)炼彪,在數(shù)據(jù)后再添加一個(gè)100的數(shù)據(jù)吐根,顏色為灰色。
需要重寫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);
}
}
這樣的話辐马,指示欄中也會(huì)相應(yīng)的多一條數(shù)據(jù)拷橘,我們需要排除這條數(shù)據(jù),通過翻源碼喜爷,我們發(fā)現(xiàn)指示欄的數(shù)據(jù)處理是在com.github.mikephil.charting.renderer.LegendRenderer
的computeLegend
方法冗疮,我們重寫這個(gè)方法
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++) {
//排除我們添加的空數(shù)據(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);
}