Scale a bitmap to fit height or width

JPARDOGO
10 min readMay 9, 2015

--

One of the most common cases in android development is the need of loading images on screen. If we don´t load them in an efficient way, then we can quickly consume the amount of memory assign to our application, causing the famous OutofMemoryError.

java.lang.OutofMemoryError: bitmap size exceeds VM budget.

But in this post I am not going to speak about how to manage bitmaps in the android platform. We can find a good explanation of how to Display Bitmaps Efficiently on the android documentation, and also a really awesome 3rd party libraries for this. Picasso is one of these libraries, in my opinion the best, that’s why I use it in my applications and I will use it on this post to show you how to resize a bitmap to target an specific height or width.

Let’s describe this scenario with an example. We have to fetch an image from a URL and this image is massive. So we have something like this:

<ScrollView   xmlns:android=”http://schemas.android.com/apk/res/android"
android:layout_width=”match_parent”
android:layout_height=”wrap_content”>
<LinearLayout
android:orientation=”vertical”
android:layout_width=”match_parent”
android:layout_height=”match_parent”>
<ImageView
android:id=”@id/image”
android:layout_width=”match_parent”
android:layout_height=”wrap_content” />
<TextView
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:padding=”@dimen/default_padding”
android:text=”@string/becon_ipsum” />
</LinearLayout>
</ScrollView>

The first thing we need to know is that _if we want to display a big resolution bitmap, we need to resize it_. So it occupies in memory just the amount it should. If we don’t resize it before we display it, it is possible that we don´t see any visual difference, but the amount of memory that this bitmap will take on the heap will be much bigger and that’s one of the reasons why the OutOfMemory error can appear.
On that example we can see a big different between resizing the article bitmap and not resizing it (Memory Analysis for Android Applications):

Image not resize

We can see that when we don’t resize it,the bitmap takes 18,5 Mb in our memory heap. But, Why does it happen,when it only takes 521KB on disk?.
Well, we need to know the difference between:

1. Size on disk: The size of the bitmap on disk is its compress size, it will depends of the compression format, such as jpeg or png.

2. Size on heap: Once android is going to display this bitmap it will be place on memory decompress, and the decompress size will depend on the bitmap resolution.

In our example we have a pretty big image, with a disk size of 521KB and a resolution of 2560x1800, then if we want to calculate its size on heap we will have to apply the following formula:

//Each pixel occupies 4 bytes
int heapSize = 2560 * 1800 * 4;

The result of this formula has to be pretty close to the one we could see with MAT when we didn’t resized the bitmap and in fact the result is 18432000 bytes when in MAT was 18432016 bytes.
Like that we can see that our bitmap occupies on heap approximately 18.4 MB .

Image resized

We can notice a big different on our memory when we resize it to its final size on screen because it occupies just 3.3MB.
We were wasting 15153022 bytes, around 15MB.

Now that we know that we need to resize our bitmaps, and we know why, the next step would be to resize them properly, so we don’t display distorted bitmaps. But, How should I resize a bitmap when I don’t know its final metrics?
In some cases we don’t know the final height/width we want to display. For instance, in our example we want to display a bitmap as the article picture and this image has to be as wide as the screen but we don’t know the final height. We want to wrap the content and let the bitmap be as high as it has to be. Basically what we want is to **respect the aspect ratio**, **resize the bitmap** and **reduce the heap memory** used for it.

How do we do that?

Picasso can help us to achieve our goal in a really simple way, but we need to investigate the different options we have:

  • centerCrop()
  • fit()
  • resize(int targetWidth, int targetHeight)
  • transform(Transformation transformation)

Center Crop

As we are speaking about not to distortion the aspect ratio, centerCrop is gonna have to by apply to our ImageView. This cropping technique crop either the top and bottom or left and right so it matches the size exactly.
But, what does it happen when we apply center Crop with Picasso? Does centerCrop resize it automatically?

If we try to do this:

Picasso.with(this)
.load(R.drawable.my_image)
.centerCrop()
.into(imageView);

An exception while be throw:

if (centerCrop && targetWidth == 0) {
throw new IllegalStateException(“Center crop requires calling resize.”);
}

Center crop requires calling resize. So if we check the function resize, we find this:

/** Resize the image to the specified size in pixels. */
public Builder resize(int targetWidth, int targetHeight) {
if (targetWidth <= 0) {
throw new IllegalArgumentException(“Width must be positive number.”);
}
if (targetHeight <= 0) {
throw new IllegalArgumentException(“Height must be positive number.”);
}
this.targetWidth = targetWidth;
this.targetHeight = targetHeight;
return this;
}

We find out that when we resize a bitmap we need to define a Height and Width bigger and than 0, what make sense, but What do we do when we don’t know the final height or width? Could the function fit(), also provided by picasso help us?

Fit & resize

This function will wait until the ImageView has been measured and resize the image to exactly match its size. After the ImageView is measured, resize is called, and the width and height of the imageView will be apply to the bitmap. At the end, fit is the same as resize, but with a minimum delay waiting for imageView to be measured. That’s why cropCenter can be use in combination with it.

Looking at the result, it seems the right solution… and thats’s right in case we know the height for our imageView. But in the moment we just want to wrap the content, we find out that the bitmap won’t be apply to our imageView. We can find the explanation for that on the ´onPreDraw` function for the ´onPreDrawListener´ apply to the image view when we use the fit function:

if (width <= 0 || height <= 0) {
return true;
}
vto.removeOnPreDrawListener(this);
this.creator.unfit().resize(width, height).into(target, callback);
return true;

We can see that in case the height or width is less or equals to zero the resize method is actually not apply (otherwise It would throw the exception we saw before).
If we think for a moment about our case we realise that the height of the ImageView is 0. The ImageView’s height is set to wrap content and there is not content because the ImageView needs to be measured before it is applied. The result of this scenario is an empty ImageView.

Later on we will be able to compare both results to see the different between this technique and the final one.

Coming back to our case, we don’t know the final height. It means that to resize the bitmap, this height should be in relationship with the final width, that we want to apply, like that, we won’t distort the bitmap.

Therefore…

  • The first step is to resize the bitmap to an specific width reducing the height proportionally.
    Because in our case we want to set the height of the ImageView to wrap content, we don’t need to apply cropCenter, but just in case that we decide to apply a maxHeight property to our ImageView then cropCenter would be our second step.
  • The second step (Optional) is to apply the cropCenter property to the ImageView (do not do it with picasso as we didn’t call resize). It would be necessary to crop the top and bottom in case the final bitmap size is bigger than the maxHeight applied of the ImageView.

Now that we know what we have to do.

How can we do it?

Transform

Picasso give us the possibility to apply custom Transformation to our bitmaps. We can do whatever we want with them just passing an instance of our transformation to the transform function of its RequestCreator object.

//NOTE: As we said before, apply cropCenter to the ImageView is optional, depending if we set or not a maxHeight property to our imageView, but if we do so, remember it has to be applied as property of the imageView because we didn’t call resize.Picasso.with(this) 
.load(R.drawable.my_image)
.transform(/*here is where we pass the transformation instance*/)
.into(imageView)

To create a custom transformation we need to create a class that implements the interface com.squareup.picasso.Transformation with its methods :

Bitmap transform(Bitmap source)

  • Transform the source bitmap into a new bitmap.
  • If a new bitmap is created, recycle must be called on source.
  • Return the original if no transformation is required.

String key()

  • Returns a unique key for the transformation, used for caching purposes.
  • If the transformation has parameters (e.g. size, scale factor, etc) then these should be part of the key.

Let´s see how we can apply a transformation to our case. First of all, to reduce the height of the image proportionally we need the image ratio. The way to calculate the ratio of the image is via the source bitmap metrics and the final width/height (in this case final width) we want to achieve. Let’s go step by step:

1. Create a class that implements com.squareup.picasso.Transformation:

public class ScaleToFitWidhtHeigthTransform implements Transformation {
@Override
public Bitmap transform(Bitmap bitmap) {
return null;
}
@Override
public String key() {
return null;
}
}

When we implements Transformation we get access to the methods transformation and key commented before.

2. Create constructor:

We want to create a constructor where we pass our parameters to apply the transformation. We basically need two parameters:

1. int size: Indicates the final width or height size.
2. boolean isHeightScale: Indicates if the size parameter is the final height or width that we want.

  • true: If the size is apply as the final height of the bitmap.
  • false: If the size is apply as the final width of the bitmap.
public ScaleToFitWidhtHeigthTransform(int size, boolean isHeightScale) {
mSize = size;
this.isHeightScale = isHeightScale;
}

3. Implement transform method to manipulate the bitmap:

Here is where we actually manipulate the bitmap. The bitmap fetched for picasso (source) is received as a parameter on this function. After it is manipulated, the result bitmap has to be return and the source recycle.

It is in here where we are going to do the calculation of the new height/width (in our case the new height) of the bitmap in relationship with the fixed known height/width we want to achieve (in our case the screen width). To do that, we need to go through the following steps:

First of all, we calculate the scale factor of the bitmap, dividing the size we want to achieve by the actual size of the bitmap. For instance, because we want to change the width of the bitmap to the screen width, we divide our desirable final width (screen width) by the actual current width of the bitmap source:

scale = (float) mSize / source.getWidth();

Once we have the scale factor we need to calculate the new size of the other side of the bitmap, the height, in this case. So we multiply the height of the source bitmap by our scale factor, and rounding the final result we get the minimum height that the bitmap needs for our selected width.

newSize = Math.round(source.getHeight() * scale);

And that´s it, now we only need to create the scaled bitmap with this values…

scaleBitmap = Bitmap.createScaledBitmap(source, mSize, newSize, true);

…recycle the source….

if (scaleBitmap != source) {
source.recycle();
}

….and return the scaleBitmap.

In case we want to manage both option and scale either the width or the height, we need to have an if statement with the boolean value isHeightScale. So the final transform method would look like that:

@Override
public Bitmap transform(Bitmap source) {
float scale;
int newSize;
Bitmap scaleBitmap;
if (isHeightScale) {
scale = (float) mSize / source.getHeight();
newSize = Math.round(source.getWidth() * scale);
scaleBitmap = Bitmap.createScaledBitmap(source, newSize, mSize, true);
} else {
scale = (float) mSize / source.getWidth();
newSize = Math.round(source.getHeight() * scale);
scaleBitmap = Bitmap.createScaledBitmap(source, mSize, newSize, true);
}
if (scaleBitmap != source) {
source.recycle();
}

return scaledBitmap
}

4. Implement key method:

The final thing we need to do is return a unique key with the different parameters of the transformation. This unique key will be use for caching purposes.

@Override
public String key() {
return “scaleRespectRatio”+mSize+isHeightScale;
}

This would be the final result:

It looks the same to the fit().cropCenter() technique but in this case the only measure needed has been the ImageView’s width (and we could do the same with the height), what can be the only information available in lot of situations.

Conclusion

Every developer should take care of the heap size on his app, and a good management of bitmaps is a must when we speak about memory. From this point of view Picasso is a really nice library with plenty of possibilities that will make your life much easier as a developer. As we can see on this little example, apply customs transformations to bitmaps cannot be easier, as I said, it is just an example but you can experiment and create you own transformations. I highly encourage all of you to do so.

As It is my first blog post ever, I apologise for any mistake.

Gist: PicassoTransformation

I hope you liked it, and keep coding!

EDIT: The las version of Picasso (2.2) fit() now handles cases where either width or height is cero.

--

--