1

I know that if I am loading an image and setting it as the source in an ImageView programmatically the best thing to do is first compute the size of the image with inJustDecodeBounds, figure out how much scaling I need, and then decode with that scaling:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.my_drawable, options);

float srcHeight = options.outHeight;
float srcWidth = options.outWidth;

int inSampleSize = ...; // compute

options = new BitmapFactory.Options();
options.inSampleSize = inSampleSize;

Bitmap myBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_drawable, options);

myImageview.setImageBitmap(myBitmap);

Now if I instead want to set this in xml and I only provide a base drawable in /drawables (no other assets in /drawables-hdpi, etc.)

<ImageView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="@drawable/my_drawable"/>

I know that Android will do some kind of scaling of the resulting bitmap to make sure it fits, but will it do this efficiently as if I was decoding the bitmap myself? Or does it load the whole bitmap into memory and then scale it?

In other words, when Android is left to its own devices to re-sample an image, when I only provide a base /drawable does it do so efficiently?

tir38
  • 9,810
  • 10
  • 64
  • 107

1 Answers1

1

If you are sure about the size you want, scaling the Bitmap yourself will save memory because android won't do it, at least not the way you might expect (see edit at bottom and comments).

I think you need to do some other consideration too. When you decode the bitmap yourself, you're targeting a size, probably the size of the ImageView (therefore after the size is of the ImageView has been computed). When inflating the xml that size is not known yet. What happens if the size of the ImageView changes after you've decoded the Bitmap? Would you rescale it? In that case wouldn't be more efficient to decode a bigger Bitmap and then just scale the drawing?

One way to find out if android is scaling the Bitmap as you would is very simple , try to set a ridiculously big image as src in the xml and see what happens (spoiler alert, it'll blow up).

Digging into the source code, it seems that the Bitmap for the src of the ImageView is decoded in this method of Drawable class, it looks like it's not taking into account the size but only the screen density

public static Drawable createFromResourceStream(Resources res, TypedValue value,
        InputStream is, String srcName, BitmapFactory.Options opts) {
    if (is == null) {
        return null;
    }

    /*  ugh. The decodeStream contract is that we have already allocated
        the pad rect, but if the bitmap does not had a ninepatch chunk,
        then the pad will be ignored. If we could change this to lazily
        alloc/assign the rect, we could avoid the GC churn of making new
        Rects only to drop them on the floor.
    */
    Rect pad = new Rect();

    // Special stuff for compatibility mode: if the target density is not
    // the same as the display density, but the resource -is- the same as
    // the display density, then don't scale it down to the target density.
    // This allows us to load the system's density-correct resources into
    // an application in compatibility mode, without scaling those down
    // to the compatibility density only to have them scaled back up when
    // drawn to the screen.
    if (opts == null) opts = new BitmapFactory.Options();
    opts.inScreenDensity = Drawable.resolveDensity(res, 0);
    Bitmap  bm = BitmapFactory.decodeResourceStream(res, value, is, pad, opts);
    if (bm != null) {
        byte[] np = bm.getNinePatchChunk();
        if (np == null || !NinePatch.isNinePatchChunk(np)) {
            np = null;
            pad = null;
        }

        final Rect opticalInsets = new Rect();
        bm.getOpticalInsets(opticalInsets);
        return drawableFromBitmap(res, bm, np, pad, opticalInsets, srcName);
    }
    return null;
}

And also, in draw(Canvas canvas) method of BitmapDrawable the drawing is done with this line:

canvas.drawBitmap(bitmap, null, mDstRect, paint);

It looks like the Bitmap is used as it is, it's the destination Rect that does the scaling

EDIT

Today I also accidentally found out another interesting thing, I put an image in the drawable folder (unspecified density) and retrieved a Bitmap with BitmapFactory.decodeResourceStream(res, value, is, pad, opts), same as the one above. The Bitmap I got was much bigger than the image in the drawable folder, I think that if the density is unspecified it will assume mdpi and will even upsample the bitmap for higher target densities.

lelloman
  • 13,883
  • 5
  • 63
  • 85
  • Good idea about testing with a huge png. – tir38 Feb 20 '17 at 21:02
  • thanks, prepare to see it blowing up :) I also discovered something new today, I put it in the answer – lelloman Feb 20 '17 at 21:43
  • Yep, tried a huge image and :boom:. Also, what you say about the image size in unspecified density is hinted at in this similar question: http://stackoverflow.com/a/13120950/1650674. What happens if you put in `drawable-nodpi` instead? – tir38 Feb 21 '17 at 19:42
  • hum, good question, according to [this one](http://stackoverflow.com/a/24682368/1527232) that's how you tell android not to scale the image! I guess I won't use assets folder for that no more :P – lelloman Feb 21 '17 at 20:42