High resolution background image gives performance issues in Android - android

I want to have a background image that scales to fit any screen size in Android. The image is static and doesn't need to scroll. I made the image at 4K resolution to cover what is a likely resolution to exist on tablets in the next 2-3 years (2560 x 1600 already exist). The image is a JPG with a 137KB file size. Similar resolution images seem to work fine in Android web browsers. Why am I getting a lot of slow down in Android (on Samsung Galaxy S3, which should have plenty of CPU/RAM to handle an image like this)? I don't feel like I am doing anything out of the ordinary.
This loads the image in the XML layout. The image is currently stored in drawable-nodpi.
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/logo_background"
android:scaleType="centerCrop" />
Making different size images for each category of screen resolution is difficult as I cannot find information on what the current maximum resolution for a device in each category is only a minimum.
I want to use the same background image again and again between a variety of fragments. Is there a way to have the image resized once to the width of the screen (preferably asynchronously) and then load that resized image each time? Could this be done with Picasso?
Please don't give answers like "of course larger images result in performance issues" or link me to Google's Supporting Different Densities. This is a real issue that is going to become more of an issue as screen resolutions continue to increase. I am amazed that handling and resizing large images is not already optimised in the ImageView class, which makes me think I am doing something wrong.

The problem is that what you are trying to do is not relying on the SDK. By having one image and having to change the image on runtime, you are causing more work to be done on the UI thread in onDraw().
Of course you would be able to create a Bitmap for a specific size, but why do such complicated work when you can rely on the SDK?
Currently there are a bunch of different folders that you can use in order to get what you are looking for, and then in the future you can get a 4k image put into a specific folder. Things like this might work:
drawable-xhdpi
drawable-xxhdpi
drawable-xlarge-xhdpi - May not be specific enough for what you are trying to accomplish
drawable-sw600dp - This allows you to specify a folder for an image where the screen width is greater than 600dp. This qualifier will probably be helpful for your case, in the future where you will be using 4k images.

You dont even need Picasso mate.Here you get the screen size:
LinearLayout layout = (LinearLayout)findViewById(R.id.YOUR_VIEW_ID);
ViewTreeObserver vto = layout.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
this.layout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = layout.getMeasuredWidth();
int height = layout.getMeasuredHeight();
}
});
And here you resize your image with your new dimensions:
public Bitmap getResizedBitmap(Bitmap bm, int newHeight, int newWidth){
int width = bm.getWidth();
int height = bm.getHeight();
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
// create a matrix for the manipulation
Matrix matrix = new Matrix();
// resize the bit map
matrix.postScale(scaleWidth, scaleHeight);
// recreate the new Bitmap
Bitmap resizedBitmap = Bitmap.createBitmap(bm, 0, 0, width, height, matrix, false);
return resizedBitmap;
}
Using a matrix to resize is relatively fast. Although user1090347s answer would be best practice.

The problem is that android uses Bitmap to render images to canvas. It is like BMP image format for me. So, you have no gain from JPG format, cuz all information lost from jpg conversion are lost forever and you will end up will fullsize bitmap anyway. The problem with big resolution is that, you have to address few bytes for every pixel, no conversion applied! In particular, smaller devices have lower memory class as bigger ones. So, you have to handle the image resolution based on device screen size and memory class.
You can properly convert your background bitmap at runtime with these helper functions:
public void getScreenSizePixels(Resources resources, int widthHeightInPixels[/*2*/])
{
Configuration config = resources.getConfiguration();
DisplayMetrics dm = resources.getDisplayMetrics();
double screenWidthInPixels = (double)config.screenWidthDp * dm.density;
double screenHeightInPixels = screenWidthInPixels * dm.heightPixels / dm.widthPixels;
widthHeightInPixels[0] = (int)(screenWidthInPixels + .5);
widthHeightInPixels[1] = (int)(screenHeightInPixels + .5);
}
--
public static Bitmap getBitmap(byte[] imageAsBytes, int reqWidth, int reqHeight) {
BitmapFactory.Options opt = new BitmapFactory.Options();
opt.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(
imageAsBytes,
0,
imageAsBytes.length,
opt);
int width = opt.outWidth;
int height = opt.outHeight;
int scale = 1;
while (reqWidth < (width / scale) || reqHeight < (height / scale)) {
scale++;
}
//bitmap.recycle();
opt.inJustDecodeBounds = false;
opt.inSampleSize = scale;
Bitmap bitmap = BitmapFactory.decodeByteArray(
imageAsBytes,
0,
imageAsBytes.length,
opt);
return bitmap;
}

Related

Should I recycle my bitmaps after a resize?

I've got a utility method (below) that resizes a bitmap and gives me back a new version. Since I am doing this with quite a few images & I wanted to reduce the chance of running out of memory, I've recycled the bitmap after usage.
This works fine on almost all devices. However, I've notice on the samsung galaxy tab 3 (10 inch) and the note 10.1 (2014) I'm getting the below stack traces:
java.lang.IllegalArgumentException: Cannot draw recycled bitmaps
at android.view.GLES20Canvas.drawBitmap(GLES20Canvas.java:756)
at android.view.GLES20RecordingCanvas.drawBitmap(GLES20RecordingCanvas.java:104)
Below is my resize code:
private static Bitmap resizeBitmap(int newWidth, int newHeight, Bitmap bitmapResize) {
if (bitmapResize == null) {
return null;
}
int width = bitmapResize.getWidth();
int height = bitmapResize.getHeight();
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmapResize,
newWidth, newHeight, true);
//SHOULD I DO THIS???
bitmapResize.recycle();
return resizedBitmap;
}
I have not figured out why almost all devices work, except those 2 (there may be more). The emulator shows no issues either.
It might be worth noting, not all images give me a "Cannot draw recycled bitmaps" error. Only some. But its consistently the same images.
(In case its of use, my app runs on 2.2 upwards)
I've managed to find the solution to my issue. It turns out the original image may be passed back as an optimisation, if the width/height of the resize match the original image.
I imagine on some devices my computations resulted in me trying to resize an image to its existing size. When i recycled the "old" bitmap, i was recyling the resized one too.
The solution is to change my code to say
if (bitmapResize!=resizedBitmap )
bitmapResize.recycle();
I found my issue covered in this conversation ( which i didn't find in my initial searching for the issue)
https://groups.google.com/forum/#!topic/android-developers/M6njPbo3U0c
I had a similar issue with on of ma games and from my experience you are doing the right thing. its the right place to recycle the old bitmap and will prevent OutOfMemoryExceptions, keep in mind that this instance of Bitmap will not be usable anymore.
You may simply use input bitmap reference as output. This way your input bitmap will be overwritten and you won't need to recycle it.
Kind of:
private static Bitmap resizeBitmap(int newWidth, int newHeight, Bitmap bitmapResize) {
if (bitmapResize == null) {
return null;
}
int width = bitmapResize.getWidth();
int height = bitmapResize.getHeight();
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
return Bitmap.createScaledBitmap(bitmapResize,
newWidth, newHeight, true);
}

Android and Pixels versus Device Metrics

I have a problem that is driving me nuts.
I had 4 functions that would resize images correcly throuhout UI using density (1.5) and width/height values reported back by views, bitmaps, screen etc.
This worked excellent on SG II. But on a HTC Wildfire which reports density 0.75 ... some images got about 25% too small visually on screen... But when I decided to override the density to 1.0 those images suddenly fit while other became crisp, but no longer sized as correctly... And it is driving me nuts...
I can only conlcude hat I am some places comparing apples to pears (pixels and DIPs), but I can not get my tests to fit with what I read, so in an effort to be 100% sure of some things:
...
Will I get pixels underneath here? or DIPs? :
int bitmapWidth = bitmap.getWidth();
Suppose underneat is an imageview containing a bitmap. Pixels or DIPs?
int widthParent = view.getWidth();
Underneath is actually DIPs, right?
getContext().getResources().getDisplayMetrics().widthPixels;
...
Here is my code that is not working (this code is giving a bitmap that visually takes maybe 2/3s width where it should take 100%) If you check comments the pixels values are apparently correct, but end result is not correct:
vto.addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
mainLogo.getViewTreeObserver().removeGlobalOnLayoutListener(this);
mainLogo.setScaleType(ImageView.ScaleType.CENTER_INSIDE); // mainLogo = ImageView
SharedCode.sharedUtilScaleImage_Width(mainLogo, false);
}
}
);
// ...
public static void sharedUtilScaleImage_Width(ImageView view, boolean tryBackground)
{
Drawable drawing = null;
boolean useBackground = false;
if (drawing == null) {
drawing = view.getDrawable();
}
if (drawing == null) {
if (tryBackground) {
drawing = view.getBackground();
useBackground = true;
}
}
if (drawing == null) {
return;
}
if (!(drawing instanceof BitmapDrawable)) {
return;
}
Bitmap bitmap = ((BitmapDrawable)drawing).getBitmap();
if (bitmap == null) {
return;
}
//--
int bitmapWidth = bitmap.getWidth(); // returns 770 (which is correct checking on disk)
int bitmapHeight = bitmap.getHeight();
// float density = 1;
// density = MicApp.getContext().getResources().getDisplayMetrics().density; // 1.5
float widthScreen = MicApp.getContext().getResources().getDisplayMetrics().widthPixels; // returns 480
int widthParent = view.getWidth(); // returns 480, should be same as screen
//--
float xScale = ( (float) widthParent / (float) bitmapWidth);
Matrix matrix = new Matrix();
matrix.postScale(xScale, xScale);
//--
Bitmap scaledBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmapWidth, bitmapHeight, matrix, true);
int bitmapWidth2 = scaledBitmap.getWidth(); // 480
int bitmapHeight2 = scaledBitmap.getHeight();
//--
BitmapDrawable result = new BitmapDrawable(scaledBitmap);
//--
if (useBackground) {
LayoutParams layoutparams = new LayoutParams(bitmapWidth, bitmapHeight);
view.setLayoutParams(layoutparams);
view.setBackgroundDrawable(result);
}
else {
view.setImageDrawable(result);
}
}
There are several steps between decoding an image and displaying it. Those steps are typically used to display images from resources at the correct scale. If you put an image just in /res/drawable-mdpi/ but not in other folders it can still displayed at the correct size on devices with other densities.
If you want to load an image that is meant for mdpi(160dpi) on an x-hdpi(320dpi) device and want it to appear at the same size you'll need to double it's pixel size in effect. E.g. a 100x100 pixel image would need to be displayed at 200x200 pixels.
The places that are involved in scaling are
Decoding the image (see e.g. BitmapFactory.Options#inDensity). BitmapFactory could already decode a 100x100 png image into a 200x200 image.
The Bitmap's own density. This is a property of every bitmap and it is used to store for what density an image is meant. If BitmapFactory would decode the 100x100 mdpi image to 200x200 already that densitiy would essentially say "I'm x-hdpi". You could also disable scaling while decoding and would get a 100x100 image with mdpi density. Displaying that image would require drawing the images scaled at 2x.
The BitmapDrawable's target density which is generally the device's density. If a BitmapDrawable has to draw a Bitmap where densities don't match it will draw a scaled version.
Code from BitmapDrawable, mTargetDensity should be device density.
private void computeBitmapSize() {
mBitmapWidth = mBitmap.getScaledWidth(mTargetDensity);
mBitmapHeight = mBitmap.getScaledHeight(mTargetDensity);
}
And relevant pieces from Bitmap, mDensity is the bitmap's density
public int getScaledWidth(int targetDensity) {
return scaleFromDensity(getWidth(), mDensity, targetDensity);
}
static public int scaleFromDensity(int size, int sdensity, int tdensity) {
if (sdensity == DENSITY_NONE || sdensity == tdensity) {
return size;
}
// Scale by tdensity / sdensity, rounding up.
return ((size * tdensity) + (sdensity >> 1)) / sdensity;
}
Scaling options of e.g. ImageView. If you want to display an ImageDrawable that represents an image that is (due to scaling or actually) 200x200 px inside an ImageView that is actually 400x300 px on the screen you need to scale the image again.
Using ImageView.ScaleType.CENTER_INSIDE would draw the image at 200x200 px because CENTER_INSIDE is only scaling the image if it is larger than the view. FIT_CENTER would scale it up to 300x300. Just CENTER does not scale at all. It just centers.
CENTER_INSIDE + a 100x100 mdpi image from above would still draw it as 200x200px because of all the scaling factors stored in BitmapDrawable / Bitmap.
There are also several other places in the BitmapFactory > Bitmap > BitmapDrawable > ImageView chain like the BitmapDrawable(Bitmap bitmap) constructor that says
This constructor was deprecated in API level 4. Use BitmapDrawable(Resources, Bitmap) to ensure that the drawable has correctly set its target density.
which could affect image scaling in undesired ways.
-> to prevent all the scaling
decode without scaling
Make sure your bitmap's density is either the current device density or simply Bitmap#setDensity(Bitmap.DENSITY_NONE)
Don't apply scaling while drawing via ImageView.ScaleType.CENTER.

How to resize image with custom size

I need to resize my image with custom size. The image is taken from device camera or gallery, i tired the below code, but the image is stretched, i need the image in square shape with out any stretch.
public Bitmap decodeSampledBitmapFromFile(Bitmap bm, int boundBoxInDp) {
boundBoxInDp=300;
int height = bm.getHeight();
int width = bm.getWidth();
float scaleWidth = ((float) boundBoxInDp) / width;
float scaleHeight = ((float) boundBoxInDp) / height;
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
Bitmap resizedBitmap = Bitmap.createBitmap(bm, 0, 0, width, height,
matrix, false);
return resizedBitmap;
}
If you already have a bitmap, you could use the following code to resize:
Bitmap originalBitmap = <original initialization>;
Bitmap resizedBitmap = Bitmap.createScaledBitmap(originalBitmap, newWidth, newHeight, false);
Or you can use following library for resize image
https://github.com/biokys/cropimage
This won't fit your image in a bounding box (the failure of which is presumably what you're calling "stretch"). It will not handle rectangular bitmaps in your square bounding box, nor will it handle images smaller than the bounding box particularly well. You probably want something like:
public Bitmap decodeSampledBitmapFromFile(Bitmap bm, int boundBoxInDp) {
boundSquareInPx=convertToPixels(300);
int maxDimen = Math.max(bm.getHeight(), bm.getWidth())
float scale = (maxDimen <= boundSquareInPx) ? 1 : boundSquareInPx / (float) maxDimen;
float scaleWidth = scale * bm.getWidth();
float scaleHeight = scale * bm.getHeight();
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
Bitmap resizedBitmap = Bitmap.createBitmap(bm, 0, 0, width, height,
matrix, true);
return resizedBitmap;
}
Few notes: if you image is smaller than your bound it won't fit it- obvious modifications do that.
Secondly, dp != px; the Bitmap object returns px, so you're going to have to convert to px from dp (which is well documented elsewhere).
Use postTranslate(...) if you need to center the correspondingly cropped bitmap.
The documentation is here; this is allready the best library I know for resizing in Android- I've never needed anything else, and I've been in the game a while and work with this frequently.
If you need, in my opinion, the best introduction to working with the API efficiently: read the source code to ImageView and Drawable instances; a really worthwhile personal development exercise would be to use the SDK to implement a fading transition drawable that is center cropped, as that's rather annoyingly one of the only things missing from the Android library, and would involve a heck of a lot of the kind of coding you're trying to do above.
NB:
As you'll note, another answerer has pointed out the existence of createScaledBitmap, which is probably much clearer code; I just wanted to point out how what you were doing was basically right and how it could be improved.
Best.
Take a look at https://github.com/coomar2841/image-chooser-library/blob/d27b542d2487132b0150be382f39e9ef95aafe68/src/com/kbeanie/imagechooser/threads/MediaProcessorThread.java.
The method called compressAndSaveImage.

The way to calculate Bitmap size?

I'm trying to find the size of my image but not to load into memory. I use the flowing code
BitmapFactory.Options o = new BitmapFactory.Options();
o.inJustDecodeBounds = true;
BitmapFactory.decodeResource(a.getResources(), R.drawable.icon, o);
int width1 = o.outWidt;
int height1 = o.outHeight;
Now, I make some comparison:
Bitmap icon = BitmapFactory.decodeResource(a.getResources(), R.drawable.icon);
int width = icon.getWidth();
int height = icon.getHeight();
Why width, height is not equal to width1 and height1 ?
I'm almost certain this is because referencing that image from resources with decode the image comes pre scaled for density.
Checkout #1 here on the docs: http://developer.android.com/guide/practices/screens_support.html#DensityConsiderations
I'm not sure what is going on, but it may be that BitmapFactory applies density scaling when actually returning a bitmap (as described here), but not when it is just decoding its size. (If this is what's going on, I'd consider filing a bug report.)
You can test this theory by moving your image from the drawables directory to drawables-nodpi.

creating scaled bitmap leads to invalid image

I've created a function that scales a bitmap directly to a specific surface area. The function first gets the width and height of the bitmap and then finds the sample size closest to the required size. Lastly the image is scaled to the exact size. This is the only way I could find to decode a scaled bitmap. The problem is that the bitmap returned from BitmapFactory.createScaledBitmap(src,width,height,filter) always comes back with a width and height of -1. I've already implemented other functions that use the createScaledBitmap() method with out this error and I can not find any reason why creating a scaled bitmap would produce invalid output. I've also found that if I create a copy of the image bitmap that is mutable causes the same error. Thanks
public static Bitmap load_scaled_image( String file_name, int area) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file_name, options);
double ratio = (float)options.outWidth / (float)options.outHeight;
int width, height;
if( options.outWidth > options.outHeight ) {
width = (int)Math.sqrt(area/ratio);
height = (int)(width*ratio);
}
else {
height = (int)Math.sqrt(area/ratio);
width = (int)(height*ratio);
}
BitmapFactory.Options new_options = new BitmapFactory.Options();
new_options.inSampleSize = Math.max( (options.outWidth/width), (options.outHeight/height) );
Bitmap image = BitmapFactory.decodeFile(file_name, new_options);
return Bitmap.createScaledBitmap(image, width, height, true);
}
I added this function to scale large camera images to a specific number of mega pixels. So a typical area passed in would be 1000000 for 1 megapixel. The camera image after being decoded yields a outWidth of 1952 and a outHieght of 3264. I then calculate the ratio this way I can keep the same height to width ratio with the scaled image, in this case the ratio is 0.598... Using the ratio and the new surface area I can find the new width which is 773 and a height of 1293. 773x1293=999489 which is just about 1 megapixel. Next I calculate the sample size for which to decode the new image, in this case the sample size is 4 and the image is decoded to 976x1632. So I'm passing in a width of 773 a height of 1293.
I was having a similar problem (getting -1 for height and width of the scaled bitmap).
Following this stackOverflow thread:
Android how to create runtime thumbnail
I've tried to use the same bitmap twice while calling the function:
imageBitmap = Bitmap.createScaledBitmap(imageBitmap, THUMBNAIL_SIZE,
THUMBNAIL_SIZE, false);
For some reason, this solved my problem, perhaps it would solve yours too.

Categories

Resources