Android Tutorial: Bitmap Processing

With the Android Bitmap API, you can manipulate images in JPG, PNG or GIF format, such as by changing the color or the opacity of each pixel in the image. In addition, you can use the API to down-sample a large image to save memory. As such, knowing how to use this API is useful, even when you are not writing a photo editor or an image processing application.

This article explains how to work with bitmaps and provides an example. previous article: Android Tutorial: Recycler View

Overview

A bitmap is an image file format that can store digital images independently of the display device. A bitmap simply means a map of bits. Today the term also includes other formats that support lossy and lossless compressions, such as JPEG, GIF and PNG. GIF and PNG support transparency and lossless compression, whereas JPEG support lossy compression and does not support transparency. Another way of representing digital images is through mathematical expressions. Such images are known as vector graphics.

The Android framework provides an API for processing bitmap images. This API takes the form of classes, interfaces, and enums in the android.graphics package and its sub packages. The Bitmap class models a bitmap image. A Bitmap can be displayed on an activity using the ImageView widget.

The easiest way to load a bitmap is by using the BitmapFactory class. This class provides static methods for constructing a Bitmap from a file, a byte array, an Android resource or an InputStream. Here are some of the methods.

public static Bitmap decodeByteArray(byte[] data, int offset, 
        int length) 
public static Bitmap decodeFile(java.lang.String pathName) 
public static Bitmap decodeResource(
        android.content.res.Resources res, int id) 
public static Bitmap decodeStream (java.io.InputStream is)


For example, to construct a Bitmap from an Android resource in an activity class, you would use this code.

Bitmap bmp = BitmapFactory.decodeResource(getResources(),
        R.drawable.image1);

Here, getResources is a method in the android.content.Context class that returns the application’s resources (Context is the parent class of Activity). The identifier (R.drawable.image1) allows Android to pick the correct image from the resources.

The BitmapFactory class also offers static methods that take options as a BitmapFactory.Options object:

public static Bitmap decodeByteArray (byte[] data, int offset, 
        int length, BitmapFactory.Options opts) 
public static Bitmap decodeFile (java.lang.String pathName, 
        BitmapFactory.Options opts) 
public static Bitmap decodeResource (android.content.res.Resources 
        res, int id, BitmapFactory.Options opts) 
public static Bitmap decodeStream (java.io.InputStream is, 
        Rect outPadding, BitmapFactory.Options opts)

There are two things you can do with a BitmapFactory.Options. The first is it allows you to configure the resulting bitmap as the class allows you to down-sample the bitmap, set the bitmap to be mutable and configure its density. The second is you can use the BitmapFactory. Options to read the properties of a bitmap without actually loading the image. For example, you may pass a BitmapFactory.Options to one of the decode methods in BitmapFactory and read the size of the image. If the size is considered too large, then you can down-sample it, saving precious memory. Down-sampling makes sense for large bitmaps when it does not reduce render quality. For instance, a 20,000 x 10,000 bitmap can be down-sampled to 2,000 x 1,000 without degradation assuming the device screen resolution does not exceed 2,000 x 1,000. In the process, it saves a lot of memory.

To decode a Bitmap without actually loading the bitmap, set the inJustDecodeBounds field of the BitmapFactory.Options object to true.

BitmapFactory.Options opts = new BitmapFactory.Options()
opts.inJustDecodeBounds = true;


If you pass the options to one of the decode methods in BitmapFactory, the method will return null and simply populate the BitmapFactory.Options object that you passed. From this object, you can retrieve the bitmap size and other properties:

int imageHeight = options.outHeight; 
int imageWidth = options.outWidth; 
String imageType = options.outMimeType;

The inSampleSize field of BitmapFactor.Options tells the system how to sample a bitmap. A value greater than 1 indicates that the image should be down-sampled. For example, setting the inSampleSize field to 4 returns an image whose size is a quarter that of the original image.

Regarding this field, the Android documentation says that the decoder uses a final value based on powers of 2, which means you should only assign a power of 2, such as 2, 4, 8, and so on. However, my own test shows that this only applies to images in JPG format and does not apply to PNGs. For instance, if the width of a PNG image is 1200, assigning 3 to this field returns an image with a width of 400 pixels, which means the inSampleSize value does not have to be a power of two.

Finally, once you get a Bitmap from a BitmapFactory, you can pass the Bitmap to an ImageView to be displayed:

ImageView imageView1 = (ImageView) findViewById(...); 
imageView1.setImageBitmap(bitmap);

Bitmap Processing

The BitmapDemo application showcases an activity that shows an ImageView that displays a Bitmap that can be down-sampled. There are four bitmaps (two JPEGs, one GIF, and one PNG) included and the application provides a button to change bitmaps. The main (and only) activity of the application is shown in Figure 1.1.

Listing 1.1 shows the AndroidManifest.xml file for the application.

Listing 1.1: The AndroidManifest.xml file

<?xml version="1.0" encoding="utf-8"?> 
<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
    package="com.example.bitmapdemo" 
    android:versionCode="1" 
    android:versionName="1.0" > 
    <uses-sdk 
        android:minSdkVersion="18" 
        android:targetSdkVersion="18" /> 
    <application 
        android:allowBackup="true" 
        android:icon="@drawable/ic_launcher" 
        android:label="@string/app_name" 
        android:theme="@style/AppTheme" > 
        <activity 
            android:name="com.example.bitmapdemo.MainActivity" 
            android:label="@string/app_name" > 
            <intent-filter> 
                <action android:name="android.intent.action.MAIN"/> 
                <category 
android:name="android.intent.category.LAUNCHER"/> 
            </intent-filter> 
        </activity> 
    </application> 
</manifest>

Figure 11.1: The BitmapDemo application

There is only one activity in this application. The layout file for the activity is given in Listing 1.2.

Listing 1.2: The activity_main.xml file

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    xmlns:tools="http://schemas.android.com/tools" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" 
    android:orientation="vertical" 
    android:gravity="bottom" 
    tools:context=".MainActivity" > 

    <ImageView 
        android:id="@+id/image_view1" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:contentDescription="@string/text_content_desc"/> 

    <LinearLayout 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content" 
        android:orientation="horizontal" > 

        <TextView 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="@string/text_sample_size"/> 
        <TextView 
            android:id="@+id/sample_size" 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
          /> 
        <Button 
            android:onClick="scaleUp" 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="@string/action_scale_up" /> 

        <Button 
            android:onClick="scaleDown" 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="@string/action_scale_down" /> 
        
    </LinearLayout> 
    <LinearLayout 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content" 
        android:orientation="horizontal" > 

        <Button 
            android:onClick="changeImage" 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:text="@string/action_change_image" /> 
        <TextView 
            android:id="@+id/image_info" 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"/> 
    </LinearLayout> 
</LinearLayout>

The layout contains a LinearLayout that in turn contains an ImageView and two LinearLayouts. The first inner layout contains two TextViews and buttons for scaling up and down the bitmap. The second inner layout contains a TextView to display the bitmap metadata and a button to change the bitmap.

The MainActivity class is presented in Listing 1.3.

Listing 1.3: The MainActivity class

package com.example.bitmapdemo;
import android.app.Activity;
import android.graphics.Bitmap; 
import android.graphics.BitmapFactory; 
import android.os.Bundle;
import android.view.Menu;
import android.view.View;
import android.widget.ImageView; 
import android.widget.TextView; 
 
public class MainActivity extends Activity { 
    int sampleSize = 2;
    int imageId = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_main); 
        refreshImage(); 
    } 

    @Override 
    public boolean onCreateOptionsMenu(Menu menu) { 
        getMenuInflater().inflate(R.menu.menu_main, menu); 
        return true; 
    } 
    
    public void scaleDown(View view) { 
        if (sampleSize < 8) { 
            sampleSize++; 
            refreshImage(); 
        } 
    } 
    
    public void scaleUp(View view) { 
        if (sampleSize > 2) { 
            sampleSize--; 
            refreshImage(); 
        } 
    } 
    private void refreshImage() {
        BitmapFactory.Options options = new BitmapFactory.Options(); 
        options.inJustDecodeBounds = true; 
        BitmapFactory.decodeResource(getResources(), 
                R.drawable.image1, options); 
        int imageHeight = options.outHeight; 
        int imageWidth = options.outWidth; 
        String imageType = options.outMimeType; 
        
        StringBuilder imageInfo = new StringBuilder(); 
        
        int id = R.drawable.image1; 
        if (imageId == 2) { 
            id = R.drawable.image2; 
            imageInfo.append("Image 2."); 
        } else if (imageId == 3) { 
            id = R.drawable.image3; 
            imageInfo.append("Image 3."); 
        } else if (imageId == 4) { 
            id = R.drawable.image4; 
            imageInfo.append("Image 4."); 
        } else { 
            imageInfo.append("Image 1."); 
        } 
        imageInfo.append(" Original Dimension: " + imageWidth 
                + " x " + imageHeight); 
        imageInfo.append(". MIME type: " + imageType); 
        options.inSampleSize = sampleSize; 
        options.inJustDecodeBounds = false; 
        Bitmap bitmap1 = BitmapFactory.decodeResource(
                getResources(), id, options); 
        ImageView imageView1 = (ImageView) 
                findViewById(R.id.image_view1); 
        imageView1.setImageBitmap(bitmap1); 
        
        TextView sampleSizeText = (TextView) 
                findViewById(R.id.sample_size); 
        sampleSizeText.setText("" + sampleSize); 
        TextView infoText = (TextView) 
                findViewById(R.id.image_info); 
        infoText.setText(imageInfo.toString()); 
        
    } 
    
    public void changeImage(View view) { 
        if (imageId < 4) { 
            imageId++; 
        } else { 
            imageId = 1; 
        } 
        refreshImage(); 
    }    
}

The scaleDown, scaleUp and changeImage methods are connected to the three buttons. All methods eventually call the refreshImage method.

The refreshImage method uses the BitmapFactory.decodeResource method to first read the properties of the bitmap resource, by passing a BitmapFactory.Options whose inJustDecodeBounds field is set to true. Recall that this is a strategy for avoiding loading a large image that will take much if not all of the available memory.

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


int imageHeight = options.outHeight; 
int imageWidth = options.outWidth; 
String imageType = options.outMimeType;


Next, it sets the inJustDecodeBounds field to false and uses the sampleSize value (that the user can change by clicking the Scale Up or Scale Down button) to set the inSampleSize field of the BitmapFactory.Options, and decode the bitmap for the second time.

options.inSampleSize = sampleSize; 
        options.inJustDecodeBounds = false; 
        Bitmap bitmap1 = BitmapFactory.decodeResource(
                getResources(), id, options);

The dimension of the resulting Bitmap will be determined by the value of the in sampleSize field.

Summary

The Android Bitmap API centers around the BitmapFactory and Bitmap classes. The former provides static methods for constructing a Bitmap object from an Android resource, a file, an InputStream, or a byte array. Some of the methods can take a BitmapFactory.Options to determine what kind of bitmap they will produce. The resulting bitmap can then be assigned to an ImageView for display.

Leave a Reply

Your email address will not be published.