【Android】MPAndroidChart 定制性开发笔记

前言

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

可惜的是,产品大大一般都不会满足于此,下图才是人家需要的。
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.LegendRenderercomputeLegend方法,我们重写这个方法

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);
    }

推荐阅读更多精彩内容