【问题标题】:Drawing on Canvas - PorterDuff.Mode.CLEAR draws black! Why?在画布上绘图 - PorterDuff.Mode.CLEAR 绘制黑色!为什么?
【发布时间】:2013-08-25 14:50:59
【问题描述】:

我正在尝试创建一个简单的自定义视图:有一个由弧形路径显示的位图 - 从 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 是带有 alpha 通道的 32 位 PNG。

【问题讨论】:

  • 您找到解决方案了吗?我已经完成了一半,但是当我尝试保存位图时,在删除区域给我黑色部分。
  • 在不涉及 OpenGL 或更改 SDK 的情况下,没有解决方案。我终于决定不画位图了。

标签: android canvas drawing porter-duff


【解决方案1】:

您的代码中一切正常,除了一件事:您的背景是黑色的,因为您的窗口是不透明的。要获得透明结果,您应该在另一个位图上绘制。在您的 onDraw 方法中,请创建一个新的位图并在其上完成所有工作。之后在画布上绘制此位图。

详情及示例代码请阅读this my answer:

【讨论】:

  • 现在假设您在每个 onDraw() 上创建一个新位图并在其上绘图。听起来像杀死应用程序。最后很遗憾地说 - 但我想要实现的目标是不可能的!有一些技巧,比如在窗口顶部设置 View 绘图(这使得 Canvas 透明),但我使用的 View 与另一个 View 重叠,所以它不适合。
  • @cadavre 不可能? Nitesh Tarani 的回答对我有用
  • @cadavre 现在我看到他在 4 年后回答了。您应该将他的答案标记为已接受,尽管
【解决方案2】:

如果您有纯色背景,您只需将 Paint color 设置为背景颜色即可。例如,如果您有白色背景,您可以这样做:

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

【讨论】:

    【解决方案3】:

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

    setLayerType(LAYER_TYPE_HARDWARE, null);
    

    【讨论】:

    • 解决了换色问题,但现在我的线变形了
    【解决方案4】:

    PorterDuff.Mode.CLEAR 不适用于硬件加速。只需设置

    view.setLayerType(View.LAYER_TYPE_SOFTWARE, null); 
    

    非常适合我。

    【讨论】:

    • 从前两天开始我就遇到了这个黑色问题。谢谢老兄。它的作用就像魅力:)
    • setLayerType(LAYER_TYPE_HARDWARE, null); 在我的情况下,这个声明有效。但是在设置后,线条和位图被破坏了。
    • 终于!我希望我之前用谷歌搜索过这个,而不是尝试自己愚蠢的解决方法
    • 是的!有效! @Nitesh Tarani,你能告诉我,有什么方法可以在画布中找到绘制的路径坐标?
    • 我整天为这个问题着迷。完美运行。非常感谢
    【解决方案5】:

    解决不想要的PorterDuff效果
    首先使用最简单的方法,就像OP的问题一样,Path.arcTo(*, *, *, *, false)就足够了——注意它是arcTo,而不是addArcfalse在添加弧之前表示no 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过滤效果的位图,使用一些缓冲/目标位图[ s] 带有 alpha 通道--和setHasAlpha(true)-- 来存储 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,它们不确定,有时不能正常工作(显示为黑色透明),尤其是View whoes onDraw(Canvas) 为空, 而ImageView.onDraw(Canvas) 使用Drawable 绘制一些; b、价格昂贵,分配off-screen bitmap存储临时绘制结果,没有明确的资源回收机制线索。
    3、使用自己的bitmap[s],更适合自定义资源回收。

    有人说,PorterDuff每次画图都不分配bitmap[s]是不可能的,因为在画图/布局/测量之前无法确定位图的宽高。
    您可以使用缓冲位图 [s] 进行 PorterDuff 绘图:
    首先,分配一些足够大的位图[s]。
    然后,使用一些矩阵在位图[s] 上绘制。
    然后,将位图 [s] 绘制到带有一些矩阵的 dest 位图中。
    最后,用一些矩阵将 dest 位图绘制到画布中。

    有人推荐 setLayerType(View.LAYER_TYPE_SOFTWARE, null),这对我来说不是一个选项,因为它会导致 onDraw(Canvas) 被循环调用。

    result image source image

    【讨论】:

      【解决方案6】:

      只需保存画布并在之后恢复

      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 的目标,第二个矩形作为源

      【讨论】:

        【解决方案7】:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2018-10-20
          • 2021-03-02
          • 2019-01-30
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-01-14
          相关资源
          最近更新 更多