Handling high-resolution images

Working with images often requires the app to be able to load very large images. Photos are especially large due to modern cameras producing high resolution images from excellent camera hardware.

How to do it...

We can easily load images from files, resources, or remote sources using the BitmapFactory type:

  1. All we need in order to load a bitmap is the path or stream and one of the decode methods, such as DecodeStreamAsync:
    using (var stream = Assets.Open("bigimage.png"))
    using (var bitmap = await BitmapFactory.DecodeStreamAsync(stream)) {
      imageView.SetImageBitmap(bitmap);
    }

As images can be quite large, both in resolution and in memory size, it is often better to reduce them using subsampling:

  1. When using subsampling, we need to provide extra options to the decode methods. This requires the use of the BitmapFactory.Options type:
    var options = new BitmapFactory.Options();
  2. Once we have the options, we can set various properties. If we want to load just the dimensions of the image, we can request the following:
    options.InJustDecodeBounds = true;
  3. If the InJustDecodeBounds property is set to true, decoding the image only loads the image dimensions, not the actual image data:
    using (var stream = Assets.Open("bigimage.png")) {
      await BitmapFactory.DecodeStreamAsync(stream));
    }
  4. Once the decoding has finished, we can access the image dimensions using the OutWidth and OutHeight properties:
    var height = options.OutHeight;
    var width = options.OutWidth;
  5. To reduce the image size when loading the actual data, we make use of the InSampleSize property. This takes any value in powers of two:
    options.InSampleSize = 4;
  6. If we are reusing the same options instance, we need to remember to set the InJustDecodeBounds property to false as we want to load the image data:
    options.InJustDecodeBounds = false;
  7. Then to load the image, we can make use of the same decode method, but this time, we keep a reference to the returned decoded Bitmap instance:
    Bitmap bitmap;
    using (var stream = Assets.Open("bigimage.png")) {
      bitmap = await BitmapFactory.DecodeStreamAsync(stream);
    }
  8. Finally, we can assign the bitmap to an ImageView instance or draw it onto a Canvas instance:
    imageView.SetImageBitmap(bitmap);

Images take up large amounts of memory on a device, and may even cause the device to run out of memory altogether. To avoid this, we have to dispose of the resources as soon as we no longer need them:

  1. If we are no longer going to reference the image from .NET, we can dispose the handle:
    bitmap.Dispose();
  2. When we are finished with the image, and we never going to display it again, we can dispose the actual image data:
    bitmap.Recycle();
  3. If we want to replace an image inside an ImageView instance, we can also dispose of the old image currently displayed:
    var old = imageView.Drawable as BitmapDrawable;
    imageView.SetImageBitmap(null);
    if (old != null) {
      old.Bitmap.Recycle();
    }

How it works...

Working with images often requires the app to be able to load very large images. Photos are especially large due to modern cameras producing high resolution images from excellent camera hardware. If we try and load images with very high resolutions, especially if there are multiple images to load, we may cause the device to run out of memory.

Although the device may have a large amount of memory, only a small amount is allowed to be used by the app. Some devices, especially the older ones, only permit a maximum of 24 MB per application.

Note

Images require a fairly large amount of memory and may quickly consume the memory available to the app.

Additionally, even the images with small file sizes still consume large amounts of memory. This is due to file compression, which results in the image being decompressed into memory. Even though this limitation is not always a problem, we can have issues when developing an image-intensive app, such as an image gallery. There would be no point in loading a 3 MB file, which might decompress into 30 MB, if we were just going to show a 100 x 100 pixel thumbnail. The memory cost and processor usage would be wasted.

Subsampling allows the app to decode a large image file but only load a reduced image with a smaller memory size. To make use of subsampling, we use the BitmapFactory and BitmapFactory.Options types.

Tip

Android provides a way to load a reduced image using subsampling.

When loading bitmaps, we are usually able to make use of the various decode methods, such as DecodeStreamAsync, DecodeFileAsync, or DecodeResourceAsync. When loading subsampled images, we still use these methods, but we use the overload that takes a BitmapFactory.Options instance. Providing options to the decode method allows us to specify a subsampling size.

Tip

A new BitmapFactory.Options instance is obtained using the default constructor.

Before subsampling, we probably would want to check the size of the original image. There is no point in reducing the size of the image if it is already smaller than what we want it to be. To query the size of an image without having to decode the entire image, we set the InJustDecodeBounds property to true on the options instance. If we were going to decode a stream, we would use the DecodeStreamAsync() method and pass the stream as the first parameter and the options as the second. When the method returns, the OutWidth and OutHeight properties on the options instance will contain the image's width and height, respectively. We can use these values to calculate an optimal sampling value.

Note

Setting the InJustDecodeBounds property to true results in only the image dimensions and MIME type being loaded, not the image data.

We can use the image width and height to calculate the optimal subsampling size in powers of two. Once we have a size, we set the InSampleSize property to that value, ensuring that we have set the InJustDecodeBounds instance back to false. When the decode method returns, it will have loaded a subsampled, or smaller, version of the image that takes up far less space in memory. This is especially useful when we want to load many images into a list view. We can quickly load many large images and only consume a small amount of memory.

However, even the smaller images take up space, and if not disposed correctly, they will eventually use all the available space. In the case of a list view, this can quickly happen if the user scrolls faster than the garbage collector, which is often the case. As Xamarin.Android involves using two garbage collectors, one for .NET and one for Java, we need to ensure that we allow both to work optimally.

Tip

The bitmap data only exists in the Java world, and .NET only retains a reference to it. Disposing the .NET handle does not dispose the actual bitmap data, only the handle.

If we are going to assign an image to an ImageView instance or some other Java type, after which we won't need to access the bitmap data again, we can tell the .NET collector that it can collect the .NET handle. This is done by calling the Dispose() method on the object. This will release the bitmap data into the Java world. If we ever want to access the bitmap data again, we will have to query Java for a new reference. For example, to access the bitmap in an ImageView instance, we would use the Drawable property on the ImageView instance.

Note

The Recycle() method is used to destroy the bitmap data, allowing the Java garbage collector to clean up.

If we are going to need to reference the image again, we can retain a reference, but we need to ensure that we let the Java collector know when it can collect it. This is done by making use of the Recycle() method. We need to ensure that we aren't using the image anywhere when recycling the bitmap, as whatever tries to display the image after the image was recycled will throw an exception.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.15.94