Lesson 160. Drawing. Bitmap. Reading large images

Lesson 160. Drawing. Bitmap. Reading large images


In this lesson:

– read and display large images

When we read images from an SD card in Bitmap, it takes up much more memory than the size of the file on the SD. Because it is stored in a compressed JPG or PNG format on a disk. And when we read it, we unpack and receive a full Bitmap.

Take the 5712×2986 image as an example. We need to display it in the app.

Let’s create a project:

Project name: P1601_BitmapLarge
Build Target: Android 4.4
Application name: BitmapLarge
Package name: ru.startandroid.develop.p1601bitmaplarge
Create Activity: MainActivity

res / values ​​/dimens.xml:



	300dp

res / layout /main.xml:



	
	

Only the ImageView is 300dp in size

MainActivity.java:

package ru.startandroid.develop.p1601bitmaplarge;

import java.io.File;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.widget.ImageView;

public class MainActivity extends Activity {

  ImageView mImageView;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    mImageView = (ImageView) findViewById(R.id.imageView);
    logMemory();
    readImage();
    logMemory();
  }

  private void readImage() {
    File file = new File(Environment.
        getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),"map.jpg");
    Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
    Log.d("log", String.format("bitmap size = %sx%s, byteCount = %s", 
        bitmap.getWidth(), bitmap.getHeight(),
        (int) (bitmap.getByteCount() / 1024)));
    mImageView.setImageBitmap(bitmap);
  }

  private void logMemory() {
    Log.i("log", String.format("Total memory = %s", 
        (int) (Runtime.getRuntime().totalMemory() / 1024)));
  }
}

method readImage – reads Bitmap from a file and displays it in ImageView.

method logMemory shows how many KB of memory is consumed by our application. We will call this method before and after display to see how much memory the image occupied.

In the Download folder on your device or emulator, drop the file map.jpg.

run:

We look at the log:

Total memory = 3184
bitmap size = 5712×2986, byteCount = 66625
Total memory = 69832

Impressive! Our image took up 66 megabytes in memory! That is, to display a 300dp image on the screen, we hold 5712×2986 image in memory and it weighs 66 megabytes. An extremely unsuccessful implementation. And if you have to display several such pictures, then OutOfMemory we will not avoid.

We need to immediately reduce the image to the size we need. What are the options here?

For example, you can read it the same way we do it now, but before creating it in ImageView, call the createScaledBitmap method (Lesson 158) to bring it to the desired size. In principle, of course, it will turn out what is needed. But still, at some point, the application will take up those 66 meters in memory to get the original picture. So, we can still catch OutOfMemory if we have a memory voltage.

Here are two options for BitmapFactory.Options:

inJustDecodeBounds – helps us to know the size of the image without taking up memory

inSampleSize – when reading an image, it will reduce the size of the image to the required number of times, and on the output we will receive an already reduced copy, which will significantly save us memory. This factor must be a multiple of two.

You can read more about these options in Lesson 159.

That is, the algorithm is:
– find out the size of the image
– determine how many times we need to reduce it to get the size we want
– we read it, immediately reducing it to the dimensions we need

Help Google has kindly provided us with ready-made methods for implementing this algorithm.

Copy them from there to MainActivity.java, Slightly changing to read the file not from resources but from SD:

  public static Bitmap decodeSampledBitmapFromResource(String path,
      int reqWidth, int reqHeight) {

    // Читаем с inJustDecodeBounds=true для определения размеров
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(path, options);

    // Вычисляем inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth,
        reqHeight);

    // Читаем с использованием inSampleSize коэффициента
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeFile(path, options);
  }

  public static int calculateInSampleSize(BitmapFactory.Options options,
      int reqWidth, int reqHeight) {
    // Реальные размеры изображения 
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

      final int halfHeight = height / 2;
      final int halfWidth = width / 2;

      // Вычисляем наибольший inSampleSize, который будет кратным двум
      // и оставит полученные размеры больше, чем требуемые 
      while ((halfHeight / inSampleSize) > reqHeight
          && (halfWidth / inSampleSize) > reqWidth) {
        inSampleSize *= 2;
      }
    }

    return inSampleSize;
  }

In the method decodeSampledBitmapFromResource we just read the images with inJustDecodeBounds enabled. Thus, options contain the image size, but the image itself is not readable. Actually, that’s where we save 66 meters. Next, we pass the options object with image size data to the calculateInSampleSize method. And we also convey the width and height of the image that we need to get on the output. The calculateInSampleSize method calculates (and places in inSampleSize) the image reduction ratio. Next, we turn off inJustDecodeBounds and get a bitmap that will be reduced to the size we need.

method calculateInSampleSize the input receives an options object that contains data about the actual size of the image. Also included are reqWidth and reqHeight, in which we pass the sizes we want.

First, the method checks that the actual image sizes are more than we need. Then the ratio starts to adjust. To do this, it calculates half the width and height of the image, divides them by a factor and checks that the result fits in the dimensions we need. If it does not moisten, the coefficient increases by two and again goes to check. This cycle runs until half the width and height of the image divided by the factor get into the dimensions we need.

Or you can interpret the logic a little differently. The cycle finds us a factor whose use will give us the size that fits in. And then we roll this factor a step back to get a size that will be a little more than necessary.

As a result, this method will determine the factor so that the image is as close to the required size as possible, but there would be more of them, not smaller. The size should turn out more necessary, so as not to lose the quality of the image when displayed. The smaller the image would have to stretch, the better the quality.

We will not be able to get exactly the dimensions we need, because the coefficient must be a multiple of two.

rewrite the method readImage using these methods:

  private void readImage() {
      int px = getResources().getDimensionPixelSize(R.dimen.image_size);
      File file = new File(Environment.
        getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),"map.jpg");
      Bitmap bitmap = decodeSampledBitmapFromResource(file.getAbsolutePath(), px, px);
      Log.d("log", String.format("Required size = %s, bitmap size = %sx%s, byteCount = %s", 
          px, bitmap.getWidth(), bitmap.getHeight(), bitmap.getByteCount()));
      mImageView.setImageBitmap(bitmap);
    }

Run, look log:

Total memory = 3204
Required size = 600, bitmap size = 1428×747, byteCount = 4166
Total memory = 7412

It got much better. Based on density, our ImageView has a size of 600 px. And when reading the image, this size was taken into account. The image is now 1428×747 and weighs 4 megabytes. This is a perfectly acceptable result.

By the way, if the algorithm of the calculateInSampleSize method is not very clear, then take the real numbers and try to figure out everything on a piece of paper. The original size was 5712×2986 and the required size was 600×600.

So, we got 4 megabytes instead of 66. But this result can be slightly improved. Our image does not need transparency. Therefore, we can use the RGB_565 configuration instead of the default ARGB_8888. This will double the weight of bitmap twice (Lesson 157).

In the method decodeSampledBitmapFromResource we will add the following line before return:

options.inPreferredConfig = Bitmap.Config.RGB_565;

Run, look log:

Total memory = 3172
Required size = 600, bitmap size = 1428×747, byteCount = 2083
Total memory = 5296

It got even better, just 2 megabytes instead of 66, and the result is visually the same.

In principle, you can also createScaledBitmap to get the size you need. This can double the weight of Bitmap twice.

If, for example, we now reduce the size of ImageView to 100 dp, then we get the following data in the beam:

Total memory = 3176
Required size = 200, bitmap size = 714×374, byteCount = 521
Total memory = 3740

The image already weighs half a megabyte of everything, and the size = 714×374.

And finally, a couple of important points.

If you are writing, for example, an application – a graphics editor, then it really will need a lot of memory. In this case, you can manifest in tag add parameter largeHeap= “True”. Your application will be allocated more memory than usual. but this should be really the last resort before you optimize everything you can. You should not misuse this option and include it simply to get rid of OutOfMemory quickly.

Note the time difference between the logs we have in the app. It can be seen that image decoding can take several hundred ms. up to a few seconds. This is too long to perform the operation in the main thread. If you want to display several such pictures, then the delay UI will be quite noticeable. Therefore, decoding is recommended in separate thread. AsyncTask (Lesson 86) and AsyncTaskLoader (Lesson 135) help you.

In the next lesson:

– we use memory-cache
– We use the Picasso library




Discuss in the forum [5 replies]

Leave a Comment