为什么 Android 上 Canvas 画出的图形不够平滑?

9,540
原文链接: github.com

通过 Google 搜索我们很快就能找到这个在 StackOverflow 中被问了很多次的问题,同时答案也经常是相同的:你需要给你的 Paint 对象设置 ANTI_ALIAS_FLAG 属性。但对于大多数人来说这并不能解决问题。下面我讲讲原因。

在 Canvas 上绘制

若你需要在 Canvas 上绘制,你有两种选择。

  • 直接在 Canvas 上绘制。
  • 先在 Bitmap 上绘制再将 Bitmap 绘制到 Canvas 上。

直接在 Canvas 上绘制

在你绘制前,先设置 Paint 对象的 ANTI_ALIAS_FLAG 属性可以得到平滑的图形。

你有两种设置 ANTI_ALIAS_FLAG 属性的方式:

    Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
    //或者
    Paint p = new Paint();
    p.setAntiAlias(true);

然后通过下面代码直接在 Canvas 上绘制。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(mLeftX + 100, mTopY + 100, 100, p);
    }

直接在 Canvas 上绘制

正如你看到的,设置 ANTI_ALIAS_FLAG 属性可以产生平滑的边缘。这里它能起作用是因为默认下每当 onDraw 被调用时系统先将 Canvas 清空然后重绘所有东西。当我在下文详细讨论 ANTI_ALIAS_FLAG 的工作原理时, 你会意识到这段信息的重要性。

先在 Bitmap 上绘制再将 Bitmap 绘制到 Canvas 上

如果你需要保存这张被绘制的图形,或者你需要绘制透明的像素,有个很好的办法是先将图形绘制到 Bitmap 上然后再将 Bitmap 绘制到 Canvas 上。下面我们通过代码来实现它。

注意: 在 onDraw 方法中初始化 Bitmap 并不是一个好主意,但在这里可以增加代码可读性。

    Paint p = new Paint();
    Bitmap bitmap = null;
    Canvas bitmapCanvas = null;
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (bitmap == null) {
            bitmap = Bitmap.createBitmap(200,
                                         200,
                                         Bitmap.Config.ARGB_8888);
            bitmapCanvas = new Canvas(bitmap);
            bitmapCanvas.drawColor(
                           Color.TRANSPARENT,
                           PorterDuff.Mode.CLEAR);
        }
        drawOnCanvas(bitmapCanvas);
        canvas.drawBitmap(bitmap, mLeftX, mTopY, p);

    }

    protected void drawOnCanvas(Canvas canvas) {
        canvas.drawCircle(mLeftX + 100, mTopY + 100, 100, p);
    }

此方式实现效果如下,没有设置 ANTI_ALIAS_FLAG 的图像不够平滑,而设置了该属性的更好一点,但你还是能发现它的边缘是粗糙的。

先在 Bitmap 上绘制再将 Bitmap 绘制到 Canvas 上

上面的代码有什么错误?

我们很容易会忽视上面代码片段出现的问题。即虽然每次 onDraw 被调用时都会更新你在 Bitmap 上绘制的圆形,但理论上说,你只是在上一个图片上重绘。所以这个问题的答案是 ANTI_ALIAS_FLAG 到底是怎么工作的?

ANTI_ALIAS_FLAG 是怎么工作的?

简单来说,ANTI_ALIAS_FLAG 通过混合前景色与背景色来产生平滑的边缘。在我们的例子中,背景色是透明的而前景色是红色的,ANTI_ALIAS_FLAG 通过将边缘处像素由纯色逐步转化为透明来让边缘看起来是平滑的。

而当我们在 Bitmap 上重绘时,像素的颜色会越来越纯粹导致边缘越来越粗糙。在下面这张图片中,我们看下不断重绘 50% 透明度的红色会出现什么状况。正如你看到的,只需三次重绘,颜色就十分接近纯色了。这就是为什么设置了 ANTI_ALIAS_FLAG 后你们图形的边缘还是十分粗糙。

我该如何解决这问题?

这里有两个选择。

  • 避免重绘。
  • 在重绘前清空你的 Bitmap。

下面我修改了上文的代码,添加一行代码让它在每次重绘前先清空 Bitmap。当然,如果你觉得纯色更加符合你的需求的话,你也可以不用每次都清空 Bitmap。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (bitmap == null) {
            bitmap = Bitmap.createBitmap(200,
                                         200,
                                         Bitmap.Config.ARGB_8888);
            bitmapCanvas = new Canvas(bitmap);
        }
        bitmapCanvas.drawColor(
                  Color.TRANSPARENT,
                  PorterDuff.Mode.CLEAR); //this line moved outside if
        drawOnCanvas(bitmapCanvas);
        canvas.drawBitmap(bitmap, mLeftX, mTopY, p);
    }

    protected void drawOnCanvas(Canvas canvas) {
        canvas.drawCircle(mLeftX + 100, mTopY + 100, 100, p);
    }

现在, Bitmap 会在每次重绘前先清空。下面的图片就是代码更改后的效果。

注意: 如果不需要经常修改 Bitmap,你可以只(在 if 条件语句中)初始化并绘制 Bitmap 一次,然后在 onDraw 方法中将其绘制到 Canvas 上,这样能保证更好的性能。也意味着频繁地清空像素并绘制圆形的操作是没有必要的。

总结

  • 如需要先绘制到 Bitmap 上:
    • 你想保存图像。
    • 你想绘制透明的像素。
    • 你的图像不需要经常改变并且/或者需要耗时操作。
  • 通过设置 ANTI_ALIAS_FLAG 属性绘制平滑的边缘。
  • 避免在 Bitmap 上重绘,或者在重绘前先清空 Bitmap。