MPAndroidChart 柱状图圆角问题解决方案

2,607 阅读3分钟

背景和问题

项目中使用到了大量图表,于是采用了 MPAndroidChart 作为解决方案。不得不说,这个库非常的强大,项目中大部分需求都可以直接使用项目提供的方法解决,但是也有一些需求需要自己的定制,比如本文主题:柱状图圆角问题。

解决方案

为了更好的修改源码实现项目需求,我采用导入模块的方式引入项目,详情不表。

我们的设计小姐姐非常的细,设计的柱状图是这样的,底部直角,顶部圆角。

image.png

最开始我找到了这个圆角方案:github.com/WW-Digital/…

这里面的代码可以解决四个角全是圆角的需求,核心是修改绘制方法,将canvas的绘制方法从 drawRect 变成了 drawRoundRect。如果需求是四个角都是圆角的话可以采用此方案,代码修改量不大。

对我来说,问题是没办法只让上面绘制圆角,所以此方案不能满足我的需求。继续尝试别的方案,在这个问题下找到了局部圆角的解决方案:stackoverflow.com/questions/3… 按照@duc tan 提供的代码,顺利的解决了局部圆角的需求,核心是将 Rect 转为 Path,并通过 drawPath 绘制。通过修改下面这个方法参数就可以任意定制圆角。

roundRect(RectF rect, float rx, float ry, boolean tl, boolean tr, boolean br, boolean bl) 

事情看起来解决了,然而高亮的时候还是方的啊,这肯定不行,于是我又重写了drawHighlighted方法,将选中高亮的柱状图也做成局部圆角,最终解决了这个问题,代码如下:

@Override
public void drawHighlighted(Canvas c, Highlight[] indices) {
    BarData barData = mChart.getBarData();

    for (Highlight high : indices) {

        IBarDataSet set = barData.getDataSetByIndex(high.getDataSetIndex());

        if (set == null || !set.isHighlightEnabled()) {
            continue;
        }

        BarEntry e = set.getEntryForXValue(high.getX(), high.getY());

        if (!isInBoundsX(e, set)) {
            continue;
        }

        Transformer trans = mChart.getTransformer(set.getAxisDependency());

        mHighlightPaint.setColor(set.getHighLightColor());
        mHighlightPaint.setAlpha(set.getHighLightAlpha());

        boolean isStack = high.getStackIndex() >= 0 && e.isStacked();

        final float y1;
        final float y2;

        if (isStack) {

            if (mChart.isHighlightFullBarEnabled()) {

                y1 = e.getPositiveSum();
                y2 = -e.getNegativeSum();

            } else {

                Range range = e.getRanges()[high.getStackIndex()];

                y1 = range.from;
                y2 = range.to;
            }

        } else {
            y1 = e.getY();
            y2 = 0.f;
        }

        prepareBarHighlight(e.getX(), y1, y2, barData.getBarWidth() / 2f, trans);

        setHighlightDrawPos(high, mBarRect);

        Path path2 = roundRect(new RectF(mBarRect.left, mBarRect.top, mBarRect.right,
                mBarRect.bottom), mRadius, mRadius, true, true, false, false);

        c.drawPath(path2, mHighlightPaint);
    }
}

最后贴一下终极方案渲染器类代码和使用方法。

public class CustomBarChartRender extends BarChartRenderer {
    private final RectF mBarShadowRectBuffer = new RectF();

    private int mRadius;

    public CustomBarChartRender(BarDataProvider chart, ChartAnimator animator, ViewPortHandler viewPortHandler) {
        super(chart, animator, viewPortHandler);
    }

    public void setRadius(int mRadius) {
        this.mRadius = mRadius;
    }

    @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()));
        mShadowPaint.setColor(dataSet.getBarShadowColor());
        boolean drawBorder = dataSet.getBarBorderWidth() > 0f;

        float phaseX = mAnimator.getPhaseX();
        float phaseY = mAnimator.getPhaseY();

        if (mChart.isDrawBarShadowEnabled()) {
            mShadowPaint.setColor(dataSet.getBarShadowColor());

            BarData barData = mChart.getBarData();

            float barWidth = barData.getBarWidth();
            float barWidthHalf = barWidth / 2.0f;
            float x;

            int i = 0;
            double count = Math.min(Math.ceil((int) (double) ((float) dataSet.getEntryCount() * phaseX)), dataSet.getEntryCount());
            while (i < count) {

                BarEntry e = dataSet.getEntryForIndex(i);

                x = e.getX();

                mBarShadowRectBuffer.left = x - barWidthHalf;
                mBarShadowRectBuffer.right = x + barWidthHalf;

                trans.rectValueToPixel(mBarShadowRectBuffer);

                if (!mViewPortHandler.isInBoundsLeft(mBarShadowRectBuffer.right)) {
                    i++;
                    continue;
                }

                if (!mViewPortHandler.isInBoundsRight(mBarShadowRectBuffer.left)) {
                    break;
                }

                mBarShadowRectBuffer.top = mViewPortHandler.contentTop();
                mBarShadowRectBuffer.bottom = mViewPortHandler.contentBottom();

                c.drawRoundRect(mBarRect, mRadius, mRadius, mShadowPaint);
                i++;
            }
        }

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

        boolean isSingleColor = dataSet.getColors().size() == 1;

        if (isSingleColor) {
            mRenderPaint.setColor(dataSet.getColor());
        }

        int j = 0;
        while (j < buffer.size()) {

            if (!mViewPortHandler.isInBoundsLeft(buffer.buffer[j + 2])) {
                j += 4;
                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));
            }

            Path path2 = roundRect(new RectF(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2],
                    buffer.buffer[j + 3]), mRadius, mRadius, true, true, false, false);
            c.drawPath(path2, mRenderPaint);

            if (drawBorder) {
                Path path = roundRect(new RectF(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2],
                        buffer.buffer[j + 3]), mRadius, mRadius, true, true, false, false);
                c.drawPath(path, mBarBorderPaint);
            }
            j += 4;
        }

    }

    @Override
    public void drawHighlighted(Canvas c, Highlight[] indices) {
        BarData barData = mChart.getBarData();

        for (Highlight high : indices) {

            IBarDataSet set = barData.getDataSetByIndex(high.getDataSetIndex());

            if (set == null || !set.isHighlightEnabled()) {
                continue;
            }

            BarEntry e = set.getEntryForXValue(high.getX(), high.getY());

            if (!isInBoundsX(e, set)) {
                continue;
            }

            Transformer trans = mChart.getTransformer(set.getAxisDependency());

            mHighlightPaint.setColor(set.getHighLightColor());
            mHighlightPaint.setAlpha(set.getHighLightAlpha());

            boolean isStack = high.getStackIndex() >= 0 && e.isStacked();

            final float y1;
            final float y2;

            if (isStack) {

                if (mChart.isHighlightFullBarEnabled()) {

                    y1 = e.getPositiveSum();
                    y2 = -e.getNegativeSum();

                } else {

                    Range range = e.getRanges()[high.getStackIndex()];

                    y1 = range.from;
                    y2 = range.to;
                }

            } else {
                y1 = e.getY();
                y2 = 0.f;
            }

            prepareBarHighlight(e.getX(), y1, y2, barData.getBarWidth() / 2f, trans);

            setHighlightDrawPos(high, mBarRect);

            Path path2 = roundRect(new RectF(mBarRect.left, mBarRect.top, mBarRect.right,
                    mBarRect.bottom), mRadius, mRadius, true, true, false, false);

            c.drawPath(path2, mHighlightPaint);
        }
    }

    private Path roundRect(RectF rect, float rx, float ry, boolean tl, boolean tr, boolean br, boolean bl) {
        float top = rect.top;
        float left = rect.left;
        float right = rect.right;
        float bottom = rect.bottom;
        Path path = new Path();
        if (rx < 0) {
            rx = 0;
        }
        if (ry < 0) {
            ry = 0;
        }
        float width = right - left;
        float height = bottom - top;
        if (rx > width / 2) {
            rx = width / 2;
        }
        if (ry > height / 2) {
            ry = height / 2;
        }
        float widthMinusCorners = (width - (2 * rx));
        float heightMinusCorners = (height - (2 * ry));

        path.moveTo(right, top + ry);
        if (tr) {
            //top-right corner
            path.rQuadTo(0, -ry, -rx, -ry);
        } else {
            path.rLineTo(0, -ry);
            path.rLineTo(-rx, 0);
        }
        path.rLineTo(-widthMinusCorners, 0);
        if (tl) {
            //top-left corner
            path.rQuadTo(-rx, 0, -rx, ry);
        } else {
            path.rLineTo(-rx, 0);
            path.rLineTo(0, ry);
        }
        path.rLineTo(0, heightMinusCorners);

        if (bl) {
            //bottom-left corner
            path.rQuadTo(0, ry, rx, ry);
        } else {
            path.rLineTo(0, ry);
            path.rLineTo(rx, 0);
        }

        path.rLineTo(widthMinusCorners, 0);
        if (br) {
            //bottom-right corner
            path.rQuadTo(rx, 0, rx, -ry);
        } else {
            path.rLineTo(rx, 0);
            path.rLineTo(0, -ry);
        }

        path.rLineTo(0, -heightMinusCorners);

        path.close();//Given close, last lineto can be removed.

        return path;
    }
}

使用方法(Kotlin):

val barChartRender = CustomBarChartRender(chart,chart.animator,chart.viewPortHandler)
barChartRender.setRadius(20)
barChart.renderer = barChartRender

实现的效果真机截图如下:

image.png