PorterduffXfermode: Clear a section of a bitmap
Asked Answered
C

3

36

The goal is to draw a bitmap and over the top of something, and draw shapes that ERASE the underlying area of the bitmap.

I have a proof of concept to try and understand how I should go about this. I have found numerous hints about using:

android.graphics.PorterDuff.Mode.CLEAR

The code below creates a screen with a blue background and adds a custom view. This view draws on its canvas from bottom to top: a pink background, a bitmap with a slight inset to show the pink background, and a yellow circle for each PorterDuffXfermode.

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuffXfermode;
import android.graphics.Paint.Style;
import android.graphics.PorterDuff.Mode;
import android.graphics.RectF;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.widget.RelativeLayout;

public class Test extends Activity {
    Drawing d = null;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        
        RelativeLayout.LayoutParams rlp = null;
        
        // Create the view for the xfermode test
        d = new Drawing(this);
        rlp = new RelativeLayout.LayoutParams(600, 900);
        rlp.addRule(RelativeLayout.CENTER_IN_PARENT);
        d.setLayoutParams(rlp);
        
        RelativeLayout rl = new RelativeLayout(this);
        rl.setBackgroundColor(Color.rgb(0, 0, 255));
        rl.addView(d);
        
        // Show the layout with the test view
        setContentView(rl);
    }
    
    public class Drawing extends View {
        Paint[] pDraw = null;
        Bitmap bm = null;
        
        public Drawing(Context ct) {
            super(ct);
            
            // Generate bitmap used for background
            bm = BitmapFactory.decodeFile("mnt/sdcard/Pictures/test.jpg");
            
            // Generate array of paints
            pDraw = new Paint[16];
            
            for (int i = 0; i<pDraw.length; i++) {
                pDraw[i] = new Paint();
                pDraw[i].setARGB(255, 255, 255, 0);
                pDraw[i].setStrokeWidth(20);
                pDraw[i].setStyle(Style.FILL);
            }
            
            // Set all transfer modes
            pDraw[0].setXfermode(new PorterDuffXfermode(Mode.CLEAR));
            pDraw[1].setXfermode(new PorterDuffXfermode(Mode.DARKEN));
            pDraw[2].setXfermode(new PorterDuffXfermode(Mode.DST));
            pDraw[3].setXfermode(new PorterDuffXfermode(Mode.DST_ATOP));
            pDraw[4].setXfermode(new PorterDuffXfermode(Mode.DST_IN));
            pDraw[5].setXfermode(new PorterDuffXfermode(Mode.DST_OUT));
            pDraw[6].setXfermode(new PorterDuffXfermode(Mode.DST_OVER));
            pDraw[7].setXfermode(new PorterDuffXfermode(Mode.LIGHTEN));
            pDraw[8].setXfermode(new PorterDuffXfermode(Mode.MULTIPLY));
            pDraw[9].setXfermode(new PorterDuffXfermode(Mode.SCREEN));
            pDraw[10].setXfermode(new PorterDuffXfermode(Mode.SRC));
            pDraw[11].setXfermode(new PorterDuffXfermode(Mode.SRC_ATOP));
            pDraw[12].setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
            pDraw[13].setXfermode(new PorterDuffXfermode(Mode.SRC_OUT));
            pDraw[14].setXfermode(new PorterDuffXfermode(Mode.SRC_OVER));
            pDraw[15].setXfermode(new PorterDuffXfermode(Mode.XOR));
        }
        
        @Override
        public void onDraw(Canvas canv) {
            // Background colour for canvas
            canv.drawColor(Color.argb(255, 255, 0, 255));
            
            // Draw the bitmap leaving small outside border to see background
            canv.drawBitmap(bm, null, new RectF(15, 15, getMeasuredWidth() - 15, getMeasuredHeight() - 15), null);

            float w, h;
            w = getMeasuredWidth();
            h = getMeasuredHeight();

            // Loop, draws 4 circles per row, in 4 rows on top of bitmap
            // Drawn in the order of pDraw (alphabetical)
            for(int i = 0; i<4; i++) {
                for(int ii = 0; ii<4; ii++) {
                    canv.drawCircle((w / 8) * (ii * 2 + 1), (h / 8) * (i * 2 + 1), w / 8 * 0.8f, pDraw[i*4 + ii]);
                }
            }
        }
    }

}

This is the result of the test:

enter image description here

The CLEAR mode is the top left, which shows as black.

In another example where I was trying to use this I had a DialogFragment where CLEAR mode erased the entire DialogFragment so that the activity beneath could be seen. Hence the reason I used many different background colours in this test.

Could this possibly be clearing the pixels of the entire activity like that other example led me to believe? I would've thought only the pixels of canvas related to the view could be erased, but in my other example the pixels of the custom view, underlying image view and DialogFragment background were all cleared.

Could someone please help me understand what exactly is going on?

EDIT: I reproduced an example confirming my suspicions. When adding this exact custom view but in a DialogFragment, the underlying activity becomes visible.

enter image description here

This seems a pretty clear indicator to me that the Mode.CLEAR is somehow erasing the canvas of the views underneath as well? My guess would be the black in the first example is that of the top level view?

Cerebrate answered 5/7, 2012 at 3:55 Comment(0)
H
47

The problem is hardware acceleration. Turn it OFF for the particular View that you are painting with CLEAR. If you're using a custom view, do this in the constructors:

if (android.os.Build.VERSION.SDK_INT >= 11) 
{
     setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}

You can also call setLayerType on a view reference.

Hymnody answered 8/11, 2012 at 20:36 Comment(4)
Will it slow down drawing, won't it?Pejoration
This did fix my issue (but I do not completely understand it). I am using PorterDuff.Mode.SRC_OUT which does not appear to be one the unsupported operationsRosebud
Unbelievable, been looking for this for 1 days! Great answer, but will it be slower?Girondist
Yes, it'll be a slower, but it'll work. Unless you're doing a lot of graphical operations, you won't notice it.Hymnody
S
14

I don't see anything unexpected. In the particular case of Mode.CLEAR, both the color and alpha of the destination are cleared, which allows the black background to show. This utility allows one to experiment with various modes, colors and alpha values, and the source may offer some insight. In the (somewhat dated) image below, the CLEAR areas reveal the faint pinstripe-gray provided by the platform's PanelUI delegate.

image
(source: Composite at sites.google.com)

Sheelagh answered 5/7, 2012 at 6:9 Comment(7)
Thanks, that makes a lot of sense. Applying my problem to this though. As you say, you correctly see the underlying background in the clear area. Why do I not see the blue (I can kinda understand why not the pink) background? The blue is a different view,so why are the pixels not transparent in the custom view canvas? (Like the image above). If I put an ImageView behind it, or anything else, I still do not see it, only black. When doing the same in a DialogFragment, this method literally drew a hole in my entire Dialog (as well as the view). So is it clearing all layers for some reason?Cerebrate
Aka, how would I do exactly like your image does (show the background by clearing out an area of a drawing?Cerebrate
Try adjusting the alpha of the Src color in SrcOver mode to see the effect. The source code may also offer some insight, and there's a related example here.Sheelagh
Still no luck, see the edit. What you are saying is obviously right trashgod, I just can't grasp why it isn't transferring over into my Android code correctly?Cerebrate
Excellent example, but I still don't see anything untoward. Android has a few extra modes, but they appear to follow the stated rules. Note that CLEAR sets the destination (alpha and color) to zero, [0, 0], irrespective of the source or destination values. You can't draw with CLEAR. Moreover, the rules are binary operations that effectively flatten two layers (source and destination) into one. I sense I'm not understanding what you find anomalous.Sheelagh
Thanks again, I am seeing now the part I am finding 'anamolous' is how these modes are actually applied to Android. As I understand it, every View is visually made up of a separate draw canvas which are drawn on top of each other. The results I am seeing suggest that these blend modes are somehow applying through the canvas, into the canvasses of the views underneath. That seems illogical to me? I can't understand how / why that is happening?Cerebrate
I think the scope of this question has kind of changed, rather than it being about the modes, my question is now more about Android View canvasses and how they treat the modes. I'm kinda new here, seeing as you have given a great answer to what I originally asked, do I mark it as the correct answer and ask another question? Or do I edit the question?Cerebrate
C
0

As Romain Guy points out here: google stuff you need to write on the Bitmap, not the canvas with the circles you are trying to clear, then set it to the main Canvas in your View. So, do something like:

// Generate bitmap used for background
            bm = BitmapFactory.decodeFile("mnt/sdcard/Pictures/test.jpg");

// Create the paint and set the bitmap
Canvas temp = new Canvas(bm.getHeight(), bm.getWidth, Config.ARGB_8888);

// in onDraw()
temp.drawCircle((w / 8) * (ii * 2 + 1), (h / 8) * (i * 2 + 1), w / 8 * 0.8f, pDraw[i*4 + ii]);

// After loop
canvas.drawBitmap(....);

Hope this helps.

Counterforce answered 14/9, 2014 at 19:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.