如何在Android画布上实现剪切边界的抗锯齿?

33

我正在使用Android的android.graphics.Canvas绘制一个环形。我的onDraw方法将画布剪裁,以便为内部圆形留出空洞,然后在空洞上绘制完整的外圆:

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

结果是一个带有漂亮的抗锯齿外边缘和不整齐的丑陋内边缘的环形:

aliased

我该如何对内边缘进行反锯齿处理?

我不想通过在中间绘制一个灰色圆来作弊,因为对话框略微透明。(在其他背景上,这种透明度并不那么微妙。)


1
经过这么多年,我发现在我的Android 12 (API 31)手机上,clipPath方法默认支持抗锯齿。但是在Android 4.4(API 19)手机上,剪切区域仍然有一个不好看的边缘。Canvas的文档没有提到这个变化,所以我不知道这个变化发生在哪个API级别上。 - hqzxzwb
4个回答

21
据我所知,您无法消除剪辑区域的锯齿。
我建议使用位图遮罩代替。将粉色、白色和浅灰色前景渲染到一个位图中,将外/内圆形遮罩(灰度 alpha 通道)渲染到另一个位图中,然后使用 Paint.setXfermode 将前景位图与遮罩合并为一个带有遮罩的 alpha 通道位图。
在 ApiDemos 源代码中可以找到一个示例,在此处可以找到:这里

1
任何一个 DstOutDstInSrcOutSrcIn 中的一个都可以解决问题——这只取决于你如何构建代码。 - Roman Nurik
2
注意,当打开硬件加速时,Xfermode 在某些设备上不会正常工作。考虑为视图使用 setLayerType(LAYER_TYPE_SOFTWARE, null) - Dmitry Zaytsev

5

我知道这不是一个通用的答案,但在这种特殊情况下,您可以使用粗线条宽度绘制弧形,而不是使用圆形+掩码。


3
我也遇到了同样的问题。我尝试使用位图遮罩(xFermode)来解决锯齿问题,但效果不佳。
因此,对于API<19,我使用位图遮罩方法,而对于API≥19,则使用了Path.Op。我没有剪裁路径然后绘制形状,而是对路径和形状(类型为Path)执行了REVERSE_DIFFERENCE操作。在API 19及以上版本中,可以对Path执行操作。
这对我来说效果完美!

1
@iscariot 这很简单。https://developer.android.com/reference/android/graphics/Path.html#op(android.graphics.Path,%20android.graphics.Path,%20android.graphics.Path.Op) - Henry
这应该是正确的答案,相同的解决方案。有三种方法可以绘制圆角:1.剪切路径(无法修复反锯齿)2.xFermode(代码更多)3.绘制路径(需要一些数学)。 - yw07

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

}

使用Xfermode Api与canvas.clipPath()一起使用可能会解决这个问题...结果

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接