Drawing on Canvas - PorterDuff.Mode.CLEAR draws black! Why?
Asked Answered
M

7

36

I'm trying to create a custom View which works simple: there is a Bitmap which is revealed by arc path - from 0deg to 360deg. Degrees are changing with some FPS.

So I made a custom View with overridden onDraw() method:

@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 is initialized as follows:

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

Now, everything draws great, but... the background is BLACK in whole View.

If I set canvas.drawColor(..., PorterDuff.Mode.DST) and omit canvas.drawBitmap() - the arc is drawn properly on transparent background.

My question is - how to set PorterDuff modes to make it work with transparency?

Of course bitmap is 32-bit PNG with alpha channel.

Multiangular answered 22/8, 2013 at 18:8 Comment(2)
have you found the solution? i am half of the way but when i am trying to save bitmap that gives me black part at remove area.Pinchhit
There's no solution for this without involving OpenGL or changing SDK. I finally decided non to draw bitmap at all.Multiangular
P
76

PorterDuff.Mode.CLEAR doesn't work with hardware acceleration. Just set

view.setLayerType(View.LAYER_TYPE_SOFTWARE, null); 

Works perfectly for me.

Psalmist answered 17/6, 2017 at 18:14 Comment(2)
setLayerType(LAYER_TYPE_HARDWARE, null); In my case, this statement worked. But after setting this the lines and bitmaps are destroyed .Algicide
for me, just as @Algicide pointed out. setLayerType(LAYER_TYPE_HARDWARE, null); works for any PorterDuffXfermode set to my paint. In my own particular scenario I am setting a linear gradient shader to my paint after applying a PorterDuff.Mode.DST_OUT. setLayerType(LAYER_TYPE_SOFTWARE, null); wipes the canvas completely transparent and then setLayerType(LAYER_TYPE_NONE, null); appears to be the default set with everything being faded into black instead of my view having a transparent fading edge.Sheply
P
19

Use this statement during initialization of the view

setLayerType(LAYER_TYPE_HARDWARE, null);
Pontiac answered 20/2, 2017 at 4:57 Comment(1)
It solved the color change problem, but now my line got distoretedAlgicide
A
3

In Jetpack Compose you can use 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
    )
}
Ard answered 10/9, 2021 at 22:39 Comment(0)
B
2

Everything is ok in your code except one thing: you get black background because your window is opaque. To achieve transparent result you should draw on another bitmap. In your onDraw method please a create new bitmap and do all the staff on it. After that draw this bitmap on your canvas.

For details and sample code please read this my answer:

Byrom answered 13/11, 2013 at 20:3 Comment(3)
Now imagine you create on each onDraw() a new Bitmap and draw on it. Sounds like killing the app. Finally its sad to say - but what I wanted to achieve is impossible! There was some tricks like setting View drawing on top of window (which made Canvas transparent) but I used View which overlapped another View so it doesn't fit.Multiangular
@Multiangular Impossible? Nitesh Tarani's answer worked for meAmbroid
@Multiangular Now i see that he answered 4 years later. You should mark his answer as the accepted one, thoAmbroid
W
1

to solve undesired PorterDuff effect
use the simplest method at first, like the OP's problem, a Path.arcTo(*, *, *, *, false) is enough -- note it's arcTo, not addArc, and the false means no forceMoveTo before adding arc -- there is no need for 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);
}

if you really need PorterDuff, mainly for complex color morphing, like blending gradients, don't draw color or shape or bitmap with PorterDuff filtering effect directly to the default canvas provided in onDraw(Canvas), use some buffering/dest bitmap[s] with alpha channel--and setHasAlpha(true)-- to store the result from PorterDuff filtering, at last draw the bitmap to the default canvas without applying any filtering except matrix changing.
here's a working example to create border blurred round image:

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

some notes: 1, this view extends ImageView not View, there are some differences.
2, why drawWithLayers --saveLayer or saveLayerAlpha-- is unrecommended: a, they are uncertain, sometimes doesn't work right(show transparent as black), especially for View whoes onDraw(Canvas) is empty, while ImageView.onDraw(Canvas) used a Drawable to draw some; b, they are expensive, they allocate off-screen bitmap to store temporary drawing result, and there is no clear clues of any resource recycling mechanism.
3, using your own bitmap[s], is better for customized resource recycling.

Some people said, it's impossible to used PorterDuff without allocation bitmap[s] every drawing, because the bitmap's width, height can't be determined before drawing/layout/measure.
you CAN use buffering bitmap[s] for PorterDuff drawing:
at first, allocate some big enough bitmap[s].
then, draw on the bitmap[s] with some matrix.
and the, draw the bitmap[s] into a dest bitmap with some matrix.
at last, draw the dest bitmap into canvas with some matrix.

some people recommend setLayerType(View.LAYER_TYPE_SOFTWARE, null), which is not an option for me, because it will cause onDraw(Canvas) to be called in a loop.

result image source image

Wrote answered 22/6, 2017 at 13:50 Comment(0)
V
0

If you have solid color background all you need to do is set Paint color to your background color. For example, if you have a white background you can do:

paint.setColor(Color.WHITE);

However, if you need to erase a line with a transparent background you try this: In order to draw with a transparent color you must use Paint setXfermode which will only work if you set a bitmap to your canvas. If you follow the steps below you should get the desired result.

Create a canvas and set its bitmap.

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

Now you should be able draw with a transparent color using:

mCanvas.drawPath(path, mPaint);
Visible answered 8/12, 2015 at 10:0 Comment(0)
A
0

just save canvas and restore after

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

in this case first rectangle will as destination for https://developer.android.com/reference/android/graphics/PorterDuff.Mode and the second rectangle as source

Airspace answered 2/10, 2018 at 5:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.