Home » Android » How do I antialias the clip boundary on Android's canvas?

How do I antialias the clip boundary on Android's canvas?

Posted by: admin April 23, 2020 Leave a comment

Questions:

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.)

How to&Answers:

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.

Answer:

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.

Answer:

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!

Answer:

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