33

I'm using Android's android.graphics.Canvas class to draw a ring. My onDraw method clips the canvas to make a hole for the inner circle, and then draws the full outer circle over the hole:

    clip = new Path();
    clip.addRect(outerCircle, Path.Direction.CW);
    clip.addOval(innerCircle, Path.Direction.CCW);

    canvas.save();
    canvas.clipPath(clip);
    canvas.drawOval(outerCircle, lightGrey);
    canvas.restore();

The result is a ring with a pretty, anti-aliased outer edge and a jagged, ugly inner edge:

aliased

What can I do to antialias the inner edge?

I don't want to cheat by drawing a grey circle in the middle because the dialog is slightly transparent. (This transparency isn't as subtle on on other backgrounds.)

Community
  • 1
  • 1
Jesse Wilson
  • 39,078
  • 8
  • 121
  • 128
  • 1
    After so many years, I found that `clipPath` method supports antialiasing by default on my Android 12 (API 31) phone. But on Android 4.4(API 19) phone the clipped area still has a bad-looking edge. The documentation of Canvas did not mention about this change, so I do not know on what API level did this change happen. – hqzxzwb Jul 29 '22 at 03:24

4 Answers4

21

As far as I know, you can't antialias clip regions.

I'd suggest using bitmap masking instead. Render the the pink, white, and light gray foreground to one bitmap, render the outer/inner circle mask (the grayscale alpha channel) to another bitmap, and then use Paint.setXfermode to render the foreground bitmap with the mask as its alpha channel.

An example can be found in the ApiDemos source code here.

double-beep
  • 5,031
  • 17
  • 33
  • 41
Roman Nurik
  • 29,665
  • 7
  • 84
  • 82
  • 1
    Either of the `DstOut`, `DstIn`, `SrcOut`, `SrcIn` would do the trick -- it just depends on how you structure the code. – Roman Nurik May 01 '10 at 01:36
  • 2
    Note that Xfermode doesn't works correctly on some devices when hardware acceleration is turned on. Consider using `setLayerType(LAYER_TYPE_SOFTWARE, null)` for view. – Dmitry Zaytsev Jan 08 '13 at 11:20
5

I know this is not a general answer, but in this particular case, you could draw arcs with a thick stroke width, instead of the circles + mask.

HRJ
  • 17,079
  • 11
  • 56
  • 80
3

I too had this same problem. I tried using Bitmap masking (xFermode) to fix the Aliasing, but it was heavy.

So for API < 19, I used the Bitmap masking way and for API >= 19, I used Path.Op. Instead of clipping the path and then drawing the shape. I did a REVERSE_DIFFERENCE of the path and the shape(which is of type Path). You can perform operations on Path from API 19 and above.

Works perfectly for me!

Henry
  • 17,490
  • 7
  • 63
  • 98
  • 1
    @iscariot it's straightforward. https://developer.android.com/reference/android/graphics/Path.html#op(android.graphics.Path,%20android.graphics.Path,%20android.graphics.Path.Op) – Henry Jan 25 '17 at 08:28
  • This should be the correct answer, same solution. Three ways to draw round corner, 1.Clippath ( cannot fix anti-alias)2. xFermode (much more code )3. Drawpath(little math) – yw07 Nov 23 '20 at 14:47
1

you can try the following code:

public class GrowthView extends View {
private static final String TAG = "GrowthView";
private int bgColor = Color.parseColor("#33485d");
private int valColor = Color.parseColor("#ecb732");
private int[] scores = new int[]{0, 10, 80, 180, 800, 5000, 20000, 50000, 100000};

private Context mContext;

private float w;
private float h;

private Paint bgPaint;
private Paint growthPaint;
private Paint textPaint;
private Paint clipPaint;
private Path bgPath;
private Path bgClipPath;
private Path growthPath;

private int growthValue = 0;

private float bgFullAngle = 240.0f;
private float gapAngle = bgFullAngle / (scores.length - 1);

private float gapRadius = 21.5f;//实际为21px 略大半个像素避免path无法缝合error
private float outerRadius = 240.0f;
private float innerRadius = outerRadius - gapRadius * 2;

private RectF outerRecF;
private RectF innerRecF;
private RectF leftBoundRecF;
private RectF rightBoundRecF;

public GrowthView(Context context) {
    this(context, null);
}

public GrowthView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public GrowthView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    this.mContext = context;
    init();
}

private void init() {
    Xfermode xFermode = new PorterDuffXfermode(PorterDuff.Mode.DARKEN);

    bgPaint = new Paint();
    bgPaint.setStyle(Paint.Style.FILL);
    bgPaint.setColor(bgColor);
    bgPaint.setStrokeWidth(0.1f);
    bgPaint.setAntiAlias(true);

    growthPaint = new Paint();
    growthPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    growthPaint.setColor(valColor);
    growthPaint.setStrokeWidth(1f);
    growthPaint.setAntiAlias(true);

    clipPaint = new Paint();
    clipPaint.setStyle(Paint.Style.FILL);
    clipPaint.setColor(Color.WHITE);
    clipPaint.setStrokeWidth(.1f);
    clipPaint.setAntiAlias(true);
    clipPaint.setXfermode(xFermode);

    textPaint = new Paint();
    textPaint.setTextSize(96);//todo comfirm the textSize
    textPaint.setStrokeWidth(1f);
    textPaint.setAntiAlias(true);
    textPaint.setTextAlign(Paint.Align.CENTER);
    textPaint.setColor(valColor);



    bgPath = new Path();
    growthPath = new Path();

    //todo 暂定中心点为屏幕中心
    DisplayMetrics metrics = new DisplayMetrics();
    WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
    wm.getDefaultDisplay().getMetrics(metrics);
    w = metrics.widthPixels;
    h = metrics.heightPixels;

    outerRecF = new RectF(w / 2 - outerRadius, h / 2 - outerRadius, w / 2 + outerRadius, h / 2 + outerRadius);
    innerRecF = new RectF(w / 2 - innerRadius, h / 2 - innerRadius, w / 2 + innerRadius, h / 2 + innerRadius);

    rightBoundRecF = new RectF(w / 2 + (float) Math.pow(3, 0.5) * (innerRadius + gapRadius) / 2 - gapRadius,
            h / 2 + (innerRadius + gapRadius) / 2 - gapRadius,
            w / 2 + (float) Math.pow(3, 0.5) * (innerRadius + gapRadius) / 2 + gapRadius,
            h / 2 + (innerRadius + gapRadius) / 2 + gapRadius);

    leftBoundRecF = new RectF(w / 2 - (float) Math.pow(3, 0.5) * (innerRadius + gapRadius) / 2 - gapRadius,
            h / 2 + (innerRadius + gapRadius) / 2 - gapRadius,
            w / 2 - (float) Math.pow(3, 0.5) * (innerRadius + gapRadius) / 2 + gapRadius,
            h / 2 + (innerRadius + gapRadius) / 2 + gapRadius);

    bgClipPath = new Path();
    bgClipPath.arcTo(innerRecF, 150.0f, 359.9f, true);
    bgClipPath.close();
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //bg
    float startAngle = 150.0f;
    float endRecfFullAngle = 180.0f;
    bgPath.arcTo(outerRecF, startAngle, bgFullAngle, true);
    bgPath.arcTo(rightBoundRecF, 30.0f, endRecfFullAngle, true);
    bgPath.arcTo(innerRecF, startAngle, bgFullAngle);
    bgPath.arcTo(leftBoundRecF, -30.0f, endRecfFullAngle);
    bgPath.rMoveTo(w / 2 - outerRadius * (float) Math.pow(3, 0.5) / 2, h / 2 + outerRadius / 2);
    bgPath.setFillType(Path.FillType.WINDING);
    bgPath.close();

    //growth
    if (getGrowthVal() != 0) {
        float temp = getGrowthAngle(getGrowthVal());
        growthPath.arcTo(outerRecF, startAngle, temp, true);
        growthPath.arcTo(getDynamicRecF(getGrowthVal()), getDynamicOriginAngle(getGrowthVal()), endRecfFullAngle, true);
        growthPath.arcTo(innerRecF, startAngle, temp);
        growthPath.arcTo(leftBoundRecF, -30.0f, endRecfFullAngle);
        growthPath.rMoveTo(w / 2 - outerRadius * (float) Math.pow(3, 0.5) / 2, h / 2 + outerRadius / 2);
        growthPath.close();
    }
    canvas.drawText(formatVal(getGrowthVal()), w / 2, h / 2, textPaint);
    canvas.clipPath(bgClipPath, Region.Op.DIFFERENCE);
    canvas.drawPath(bgPath, bgPaint);
    canvas.drawPath(growthPath, growthPaint);
    canvas.drawPath(bgClipPath, clipPaint);
}

private float getDynamicOriginAngle(int growthVal) {
    return growthVal <= 30 ? getGrowthAngle(growthVal) + 150 :
            getGrowthAngle(growthVal) - 210;
}

private RectF getDynamicRecF(int growthVal) {
    float dynamicAngle = getGrowthAngle(growthVal);
    //动态圆心
    float _w = w / 2 + (float) Math.sin(Math.toRadians(dynamicAngle - 120)) * (outerRadius - gapRadius);
    float _y = h / 2 - (float) Math.sin(Math.toRadians(dynamicAngle - 30)) * (outerRadius - gapRadius);
    return new RectF(_w - gapRadius, _y - gapRadius, _w + gapRadius, _y + gapRadius);
}

private int getGrowthVal() {
    return this.growthValue;
}

public void setGrowthValue(int value) {
    if (value < 0 || value > 100000) {
        try {
            throw new Exception("成长值不在范围内");
        } catch (Exception e) {
            Log.e(TAG, e.getMessage());
            e.printStackTrace();
        }
    }
    this.growthValue = value;
    invalidate();
}

private float getGrowthAngle(int growthVal) {
    return gapAngle * (getLevel(growthVal) - 1)
            + gapAngle * (growthVal - scores[getLevel(growthVal) - 1]) /
            (scores[getLevel(growthVal)] - scores[getLevel(growthVal) - 1]);
}

private int getLevel(int score) {
    return score < 0 ? -1 : score <= 10 ? 1 : score <= 80 ? 2 : score <= 180 ? 3 : score <= 800 ?
            4 : score <= 5000 ? 5 : score <= 20000 ? 6 : score <= 50000 ? 7 : 8;
}

private String formatVal(int value) {
    StringBuilder builder = new StringBuilder(String.valueOf(value));
    return value < 1000 ? builder.toString() : builder.insert(builder.length() - 3, ',').toString();
}

}

Use the Xfermode Api with canvas.clipPath() may resolve this problem... Result

Zhang Xiang
  • 422
  • 4
  • 15