bmarrdev / android-DecoView-charting

DecoView: Android arc based animated charting library
Apache License 2.0
990 stars 194 forks source link

Convex and concave endcaps #5

Open nealsanche opened 8 years ago

nealsanche commented 8 years ago

I've had a request to have a concave endcap at the start of a data series, and a convex endcap at the end of the series. I've only been able to approximate such a thing by adding another series at the top layer, which just draws a dot, a little larger than the main data series width. Then I show and hide that layer at times when I know the data will overlap. This doesn't really work very well, as you can see below:

hammerheadmpa44inealsanche08312015162059

The area between about 92% and 100% is pretty much undefined. If I remove the grey circle, the endcap of the start of the data series shows, and looks odd.

I'm considering forking this repository to add this alternate endcap style, but thought I'd ask first in case there was another way this could be achieved in the library that I haven't discovered yet.

nealsanche commented 8 years ago

I did have another idea of how to do this with one additional series that would only be drawn from about 20% to 100%, one layer above the grey dot. However, I couldn't find a way to have it not display from 0% to 20%, so it still didn't work to achieve the desired effect.

nealsanche commented 8 years ago

I think I can actually make this happen by putting two charts into a FrameLayout and synchronizing the series to overlap correctly. Seems workable anyway.

bmarrdev commented 8 years ago

There is no alternative way to achieve this currently.

One feature I am considering is to introduce customizable caps for the start and end of the series. I may also consider exposing ability to change start angle for each data series.

When implemented these caps would be configured when building the SeriesItem and would replace the current setCapRounded(boolean) setting with an enumerated cap type such as CAP_CONCAVE, CAP_CONVEX, CAP_POINT, CAP_ARROW...

Unfortunately I am busy with work for a client at the moment so there is no set schedule for this feature.

If you want to implement your concave start cap for yourself take a look at the LineSeriesArc.draw() function. You can use the canvas clipping functions in here to clip the drawing area of the canvas before the line series is drawn and restore it after the draw is finished.

This would be done with something like:

public boolean draw(Canvas canvas, RectF bounds) {
...
        canvas.save();
        processConcaveDraw(canvas);
        drawArc(canvas);
        canvas.restore();
...
    protected void processConcaveDraw(@NonNull Canvas canvas) {
        float lineWidth = (getSeriesItem().getLineWidth() * 0.6f);
        float radius = mBoundsInset.width() / 2;
        float angle = (float)(Math.PI *  mArcAngleStart / 180.0f);
        float middleX = (float)(mBoundsInset.centerX() + radius * Math.cos(angle));
        float middleY = (float)(mBoundsInset.centerY() + radius * Math.sin(angle));
        Path mPath = new Path(); //TODO: don't new Path on every draw
        mPath.addCircle(middleX, middleY, lineWidth, Path.Direction.CW);
        canvas.clipPath(mPath, Region.Op.DIFFERENCE);
    }

I made the radius here larger (60% of line width rather than 50%) than the exact size of the start cap of the line as antialiasing would probably leave some artifacts otherwise.

This will clip a concave circle into the start of the arc. The problem will be that when the arc is drawn as a full circle the end of the arc will also be clipped.

As a quick workaround, if you are not using a semi-transparent color for your arc line you could simply add a second drawArc after the canvas.restore() call that will draw over the end of the arc but not the start. For example you could offset the second arc to draw only from 180 degrees to the end point:

            if (mArcAngleSweep > 270) {
                canvas.drawArc(mBoundsInset,
                        mArcAngleStart + 180,
                        mArcAngleSweep - 180,
                        false,
                        mPaint);
            }

With some more research required I believe an alternative approach may be to use an antialiased paint with PorterDuff.Mode.CLEAR rather than canvas clipping.

nealsanche commented 8 years ago

So, I've taken your ideas and tried them. Clipping a circle using the path clipping does work, but has the exact problem you outlined. The quick workaround really doesn't work for semi-transparent arcs, which you noted. I'll see if I can figure out this CLEAR paint method, and see if that is workable.

One other deficiency is that the clipped circle diameter doesn't quite match the round end-cap diameter, so the arc doesn't quite close at 100%. Here's what I have after a bit of tuning:

hammerheadmpa44inealsanche09012015112502

With non-transparent paint, this is very nearly what I need, but wouldn't be generic enough for the rest of your users, most likely.

nealsanche commented 8 years ago

Did a little polishing. Unfortunately, as I polish, it makes your code somewhat uglier, but I sent a PR https://github.com/bmarrdev/android-DecoView-charting/pull/6 just to show you, and you can do something similar but better.

I solved the double drawn circle at 320 degrees, and also made it work with edge effects.

I tried putting in some CLEAR paint, but it just kept looking like a darker spot on the ring. It didn't seem to actually erase the pixels at that spot. So I had to draw the ring in two passes, and path mask the drawing twice to avoid overlap.