在画布上使用PorterDuff.Mode.CLEAR绘制会变成黑色!为什么?

35

我试图创建一个简单的自定义视图:有一个位图,它通过弧形路径显示 - 从0度到360度。度数会以一定的FPS变化。

因此,我创建了一个重写了onDraw()方法的自定义视图:

@Override
protected void onDraw(Canvas canvas) {

    canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
    arcPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
    canvas.drawArc(arcRectF, -90, currentAngleSweep, true, arcPaint);
    arcPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    canvas.drawBitmap(bitmap, circleSourceRect, circleDestRect, arcPaint);
}

arcPaint 的初始化如下:

arcPaint = new Paint();
arcPaint.setAntiAlias(true);
arcPaint.setColor(Color.RED); // Color doesn't matter

现在,一切都很好,但是整个视图的背景是黑色的。

如果我设置canvas.drawColor(..., PorterDuff.Mode.DST)并省略canvas.drawBitmap() - 则圆弧将在透明背景上正确绘制。

我的问题是 - 如何设置PorterDuff模式使其与透明度一起正常工作?

当然,bitmap是带有α通道的32位PNG。


1
你找到解决方案了吗?我已经完成了一半,但是当我尝试保存位图时,在删除区域会出现黑色部分。 - Mayur R. Amipara
这个问题没有解决方案,除非涉及OpenGL或更改SDK。我最终决定根本不绘制位图。 - cadavre
7个回答

75
< p > PorterDuff.Mode.CLEAR在硬件加速下无法工作。只需设置

view.setLayerType(View.LAYER_TYPE_SOFTWARE, null); 

对我来说完美地运作。


2
setLayerType(LAYER_TYPE_HARDWARE, null); 在我的情况下,这个语句是有效的。但是在设置之后,线条和位图被破坏了。 - noobEinstien
对于我来说,就像@noobEinstien指出的那样。setLayerType(LAYER_TYPE_HARDWARE, null);适用于我所设置的任何PorterDuffXfermode。在我自己的特定场景中,我在应用PorterDuff.Mode.DST_OUT后,将线性渐变着色器设置给我的画笔。setLayerType(LAYER_TYPE_SOFTWARE, null);会将画布完全变为透明,然后setLayerType(LAYER_TYPE_NONE, null);似乎是默认设置,所有内容都会淡入黑色,而不是我的视图具有透明的渐变边缘。 - undefined

18

在视图初始化期间使用此语句

setLayerType(LAYER_TYPE_HARDWARE, null);

它解决了颜色变化的问题,但现在我的线条变形了。 - noobEinstien

3
Jetpack Compose中,您可以使用blendMode
Canvas(modifier = Modifier.fillMaxSize()) {
    drawIntoCanvas { it.saveLayer(Rect(Offset.Zero, size), Paint()) }
    drawRect(color = Color.Blue, size = size)
    
    val clearSize = Size(200.dp.toPx(), 300.dp.toPx())
    drawRect(
        color = Color.Transparent,
        topLeft = Offset((size.width - clearSize.width) / 2, (size.height - clearSize.height) / 2),
        size = clearSize,
        blendMode = BlendMode.Clear
    )
}

2

除了一个问题,你的代码一切都很好:由于你的窗口是不透明的,所以你得到了黑色背景。为了实现透明效果,你应该在另一个位图上绘制。请在你的onDraw方法中创建一个新的位图,并在它上面进行所有操作。之后,在你的画布上绘制这个位图。

有关详细信息和示例代码,请阅读我的答案


1
现在想象一下,每次调用onDraw()时,您都会创建一个新的位图并在其上绘制。听起来像是在杀死应用程序。最后很遗憾地说 - 但是我想要实现的是不可能的!有一些技巧,比如将View绘制在窗口顶部(这使Canvas透明),但我使用的View与另一个View重叠,因此不合适。 - cadavre
@cadavre 不可能?Nitesh Tarani的答案对我有用。 - GabrielBB
1
@cadavre,现在我看到他回答了4年后。你应该将他的答案标记为被接受的。 - GabrielBB

1
为了解决不需要的PorterDuff效果,首先使用最简单的方法,就像OP的问题一样,使用Path.arcTo(*, *, *, *, false)就足够了--请注意是arcTo而不是addArc,并且false表示在添加弧之前不需要forceMoveTo--没有必要使用PorterDuff。
Path arcPath = new Path();
@Override
protected void onDraw(Canvas canvas) {
    arcPath.rewind();
    arcPath.moveTo(arcRectF.centerX, arcRectF.centerY);
    arcPath.arcTo(arcRectF, -90, currentAngleSweep, false);
    arcPath.close();
    canvas.clipPath(arcPath, Region.Op.DIFFERENCE);
    canvas.drawBitmap(bitmap, circleSourceRect, circleDestRect, arcPaint);
}

如果你确实需要PorterDuff,主要用于复杂的颜色变换,比如混合渐变,不要直接在onDraw(Canvas)提供的默认画布上使用PorterDuff过滤效果绘制颜色、形状或位图,而是使用一些带有alpha通道的缓冲/目标位图来存储PorterDuff过滤的结果,并最后将位图绘制到默认画布上,除了矩阵变换外不应用任何过滤。这里有一个可行的示例,用于创建模糊边框圆形图像:
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.widget.ImageView;

/**
* Created by zdave on 6/22/17.
*/

public class BlurredCircleImageViewShader extends ImageView {
private Canvas mCanvas;
private Paint mPaint;
private Matrix matrix;
private static final float GRADIENT_RADIUS = 600f;  //any value you like, but should be big enough for better resolution.
private Shader gradientShader;
private Bitmap bitmapGradient;
private Bitmap bitmapDest;
private Canvas canvasDest;
public BlurredCircleImageViewShader(Context context) {
    this(context, null);
}

public BlurredCircleImageViewShader(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
}

public BlurredCircleImageViewShader(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    matrix = new Matrix();
    int[] colors = new int[]{Color.BLACK, Color.BLACK, Color.TRANSPARENT};
    float[] colorStops = new float[]{0f, 0.5f, 1f};
    gradientShader = new RadialGradient(GRADIENT_RADIUS, GRADIENT_RADIUS, GRADIENT_RADIUS, colors, colorStops, Shader.TileMode.CLAMP);
    mPaint.setShader(gradientShader);

    bitmapGradient = Bitmap.createBitmap((int)(GRADIENT_RADIUS * 2), (int)(GRADIENT_RADIUS * 2), Bitmap.Config.ARGB_8888);
    bitmapDest = bitmapGradient.copy(Bitmap.Config.ARGB_8888, true);

    Canvas canvas = new Canvas(bitmapGradient);
    canvas.drawRect(0, 0, GRADIENT_RADIUS * 2, GRADIENT_RADIUS * 2, mPaint);

    canvasDest = new Canvas(bitmapDest);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int width = getMeasuredWidth();
    setMeasuredDimension(width, width);
}

@Override
protected void onDraw(Canvas canvas){
    /*uncomment each of them to show the effect, the first and the third one worked, the second show the same problem as OP's*/
    //drawWithLayers(canvas);  //unrecommended.
    //drawWithBitmap(canvas);  //this shows transparent as black
    drawWithBitmapS(canvas);   //recommended.
}
@SuppressLint("WrongCall")
private void drawWithLayers(Canvas canvas){
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
    float width = canvas.getWidth();
    float hWidth = width / 2;
    //both saveLayerAlpha saveLayer worked here, and if without either of them,
    //the transparent area will be black.
    //int count = canvas.saveLayerAlpha(0, 0, getWidth(), getHeight(), 255, Canvas.ALL_SAVE_FLAG);
    int count = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
    super.onDraw(canvas);
    float scale = hWidth/GRADIENT_RADIUS;
    matrix.setTranslate(hWidth - GRADIENT_RADIUS, hWidth - GRADIENT_RADIUS);
    matrix.postScale(scale, scale, hWidth, hWidth);
    gradientShader.setLocalMatrix(matrix);

    canvas.drawRect(0, 0, width, width, mPaint);

    canvas.restoreToCount(count);
}
@SuppressLint("WrongCall")
private void drawWithBitmap(Canvas canvas){
    super.onDraw(canvas);
    float scale = canvas.getWidth() / (GRADIENT_RADIUS * 2);
    matrix.setScale(scale, scale);
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
    canvas.drawBitmap(bitmapGradient, matrix, mPaint);  //transparent area is still black.
}
@SuppressLint("WrongCall")
private void drawWithBitmapS(Canvas canvas){
    float scale = canvas.getWidth() / (GRADIENT_RADIUS * 2);
    int count = canvasDest.save();
    canvasDest.scale(1/scale, 1/scale); //tell super to draw in 1/scale.
    super.onDraw(canvasDest);
    canvasDest.restoreToCount(count);

    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
    canvasDest.drawBitmap(bitmapGradient, 0, 0, mPaint);

    matrix.setScale(scale, scale);  //to scale bitmapDest to canvas.
    canvas.drawBitmap(bitmapDest, matrix, null);
    }
 }

一些注意事项: 1、该视图继承自ImageView而不是View,有一些区别。
2、为什么不推荐使用drawWithLayers——saveLayersaveLayerAlpha——a、它们是不确定的,有时无法正确工作(将透明显示为黑色),特别是对于onDraw(Canvas)为空的View,而ImageView.onDraw(Canvas)则使用一个Drawable来绘制一些内容;b、它们很昂贵,它们分配离屏位图来存储临时绘制结果,并且没有任何资源回收机制的明确线索。
3、使用自己的位图更好地实现自定义资源回收。

有些人说,如果不为每次绘制分配位图,则无法使用PorterDuff,因为在绘制/布局/测量之前无法确定位图的宽度和高度。
您可以使用缓冲位图进行PorterDuff绘制:
首先,分配一些足够大的位图。
然后,使用某些矩阵在位图上绘制。
接下来,使用某些矩阵将位图绘制到目标位图中。
最后,使用某些矩阵将目标位图绘制到画布中。

有些人建议设置LayerType(View.LAYER_TYPE_SOFTWARE, null),但这对我不是一个选项,因为它会导致onDraw(Canvas)被循环调用。

结果图像 源图像


0

保存画布,然后进行恢复操作

canvas.saveLayer(clipContainer, null, Canvas.ALL_SAVE_FLAG);
canvas.drawRoundRect(rectTopGrey, roundCorners, roundCorners, greyPaint);
canvas.drawRoundRect(rectTopGreyClip, roundCorners, roundCorners, clipPaint);
canvas.restore();

在这种情况下,第一个矩形将作为https://developer.android.com/reference/android/graphics/PorterDuff.Mode的目标,第二个矩形将作为源。


0
如果您有纯色背景,您只需要将画笔颜色设置为您的背景颜色即可。例如,如果您有白色背景,您可以这样做:
paint.setColor(Color.WHITE);

然而,如果您需要擦除具有透明背景的线条,则可以尝试以下方法: 为了使用透明颜色进行绘制,您必须使用Paint setXfermode,但前提是您必须将位图设置到画布上。如果按照以下步骤操作,您应该能够获得所需的结果。

创建画布并设置其位图。

mCanvas = new Canvas();
mBitmap= Bitmap.createBitmap(scrw, scrh, Config.ARGB_8888);
mCanvas.setBitmap(mBitmap);
When you want to erase something you just need to use setXfermode.

public void onClickEraser() 
{ 
   if (isEraserOn)
      mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
   else
      mPaint.setXfermode(null);
}

现在你应该能够使用透明颜色进行绘制:

mCanvas.drawPath(path, mPaint);

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