34

So I tried the code from here: Creating an ImageView with a mask. I'm using the following images as original and mask:

enter image description hereenter image description here

However, the result I get is this:

enter image description here

Note that the window background is not black, but holo light (which on the galaxy nexus looks like a very pale gray, not completely white). The second image is the result I get when an item is selected on a list view.

If instead I create a new Bitmap using the same algorithm and then pass it to the image view instead of overriding onDraw(), it draws correctly:

Canvas canvas = new Canvas();
Bitmap mainImage = //get original image
Bitmap maskImage = //get mask image
Bitmap result = Bitmap.createBitmap(mainImage.getWidth(), mainImage.getHeight(), Bitmap.Config.ARGB_8888);

canvas.setBitmap(result);
Paint paint = new Paint();
paint.setFilterBitmap(false);

canvas.drawBitmap(mainImage, 0, 0, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
canvas.drawBitmap(maskImage, 0, 0, paint);
paint.setXfermode(null);

imageView.setImageBitmap(result);

I get the expected result:

enter image description here

Note the fade is correctly applied. This is more evident when a selection is made.

So what's going on on ImageView's onDraw method to create this black backdrop instead of letting the window background show through? What's interesting is that if the original image itself has some transparency, that transparency is respected, for example:

enter image description here

I can't figure it out by myself. I'd rather be able to do it on onDraw instead of pre-creating the bitmap because it only works for bitmaps as source and mask. I want to be able to do it with other drawables like gradients and solid colours but on those cases the width and height are not set.

Community
  • 1
  • 1
AngraX
  • 1,803
  • 2
  • 18
  • 30
  • 1
    Here's an update. After reading http://stackoverflow.com/questions/3467334/erase-bitmap-parts-using-porterduff-mode I could reproduce the same black behaviour on the working example if I set the Bitmap config to RGB_565 instead of ARGB_8888. I could also narrow the issue down to the canvas being passed on the onDraw method. If I use the onDraw canvas, the black border appear. Since the canvas passed to onDraw does not have a bitmap associated, maybe natively is does not support transparency? Another thing I found out is that the black border disappear while the list view is scrolling. – AngraX Feb 11 '13 at 05:23
  • 1
    Ok. After reading this http://stackoverflow.com/questions/5231260/android-shader-behave-different-in-ondrawcanvas-and-new-canvasbitmap it seems the bug is actually that I'm making the pixels of the window canvas transparent if I apply that on the canvas passed to onDraw(). And behind the window there's a black background. It seems that I have no option but to use a temporary bitmap and render into it,unless I can find another DST mode that works. – AngraX Feb 11 '13 at 05:31
  • i am tryin to implement a similar functionality where i have one imageview with the image and the second wih black mask , on touch it should show the behind imageview in the circle as your showing , can you help me out here? – usr30911 Aug 13 '14 at 12:52

3 Answers3

29

I have found the perfect combination for creating masking without black border after researching through all the stackoverflow posts. It suits my purpose quite well.

Currently I'm creating a draggable view using one normal image and a masking image (a png with transparency), so I'll need to override the onDraw function.

private Bitmap mImage = ...;
private Bitmap mMask = ...;  // png mask with transparency
private int mPosX = 0;
private int mPosY = 0;

private final Paint maskPaint;
private final Paint imagePaint;

public CustomView (final Context context) {
    maskPaint = new Paint();
    maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));

    imagePaint = new Paint();
    imagePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER));
}

/* TODO
 if you have more constructors, make sure you initialize maskPaint and imagePaint
 Declaring these as final means that all your constructors have to initialize them.
 Failure to do so = your code won't compile.
*/

@Override
public void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.save();      
    canvas.drawBitmap(mMask, 0, 0, maskPaint);
    canvas.drawBitmap(mImage, mPosX, mPosY, imagePaint);
    canvas.restore();
}
copolii
  • 14,208
  • 10
  • 51
  • 80
morph85
  • 807
  • 10
  • 18
  • 1
    Hey. I just checked the logic after reading a few articles about xfer modes and I believe your solution works pretty well. Accepting it as the official answer. For more details about xfer modes, see: http://www.piwai.info/transparent-jpegs-done-right/ and http://ssp.impulsetrain.com/2013-03-17_Porter_Duff_Compositing_and_Blend_Modes.html – AngraX Oct 04 '13 at 15:19
  • In case the algorithm above didn't work, change maskPaint's setXfermode to PorterDuff.Mode.DST_OUT. – morph85 Nov 25 '13 at 08:29
  • It worked with both modes: CLEAR and DST_OUT. Interesting through, from the documentation I've read about xfer modes, CLEAR should clear thew whole thing, but on Android it works exactly as a DST_OUT instead. BTW, I'm using shaders now instead of xfer modes so I perform 1 less drawing pass, making my overdraw drop from reds to greens :). – AngraX Dec 09 '13 at 19:29
  • In case if you set the manifest's supports-screens - android:anyDensity to false, the CLEAR option will not work. – morph85 Dec 10 '13 at 03:28
  • I'm using DST out as it makes more sense. When you say CLEAR does not work if android:anyDensity is set to false, do you mean it now behaves as it should (clearing the whole thing)? – AngraX Dec 11 '13 at 14:26
  • Yes, it cleared everything. DST_OUT should be more accurate. But somehow when I tested, DST_OUT + anyDensity:true does not work for me. Currently I'm setting it to CLEAR + anyDensity:true (currently using Android 4.1.1 & 4.1.2 for testing). Further experimentation is required. – morph85 Dec 12 '13 at 02:59
  • 1
    @morph85 I would avoid instantiating the paint objects in onDraw. You're always going to instantiate the exact same two objects, which means your frame rate takes a hit. Declare both as finals in your class and you should get a higher frame rate. Generally avoid instantiating objects in layout, measure, and draw methods. – copolii Jul 19 '14 at 22:05
  • @copolii Yes I'm aware of that. Please make appropriate adjustments. – morph85 Jul 21 '14 at 02:46
  • @copolii How to scale/zoom/rotate the image but not the mask? Is it possible with this class? – SkyWalker Mar 31 '15 at 07:58
  • Anyone can tell if this solution works with 2 different sized images or the image and the mask must have the same size? – Lisitso Aug 12 '16 at 11:00
2

Answering my own question. The Xfermode was working as intended. The paint was making the resulting are of the canvas transparent (which was the canvas used by the window activity). Since the canvas itself was being set transparent, the window was showing what was behind it: the black background.

To do it properly, indeed a new Bitmap has to be created to hold the result of the alpha mask. I updated the code to take into account drawables of all types.

AngraX
  • 1,803
  • 2
  • 18
  • 30
0

In this Code Apply:

 mask_over = BitmapFactory.decodeResource(
            getResources(), mask_over1[0]);
    icon = Bitmap.createScaledBitmap(icon, screenwidth, screenwidth, false);
    mask_over = Bitmap.createScaledBitmap(mask_over, screenwidth, screenwidth, false);
             back_img=createBitmap_ScriptIntrinsicBlur(Bitmap.createScaledBitmap(cropview.croppedImage, screenwidth, screenwidth, false),25.0f);
    LinearLayout.LayoutParams layoutParams111 = new LinearLayout.LayoutParams(screenwidth, screenwidth);
Ramani Hitesh
  • 214
  • 3
  • 15