Android Bitmap Scaling

2011/01/27

Scaling images is a common task for many applications including those written for Android. The most obvious approach, as described on this page, won’t work in most cases. The problem as you would soon find out if you tried it is that your phone doesn’t have enough memory. Luckily we can get around that, but not easily.

The trick ends up being a two step process. First we decode the bitmap, setting the sampling level as high as we can while avoiding loss of quality, then we scale this smaller bitmap in memory down to the exact specified size. I’ve included this in a nicely packaged class BitmapScaler, included at the end of this post.The class itself looks quite long but this is only because it has the flexibility of scaling bitmaps from resources, assets, and files.

BitmapScaler preserves the aspect ratio. You may only pass a new width. The resulting height will be scaled equal to the amount the width was scaled.

For example,

BitmapScaler scaler = new BitmapScaler(getResources(), R.drawable.moorwen, newWidth);
imageView.setImageBitmap(scaler.getScaled());

BitmapScaler class follows,

class BitmapScaler {
	private static class Size {
		int sample;
		float scale;
	}

	private Bitmap scaled;

	BitmapScaler(Resources resources, int resId, int newWidth)
			throws IOException {
		Size size = getRoughSize(resources, resId, newWidth);
		roughScaleImage(resources, resId, size);
		scaleImage(newWidth);
	}

	BitmapScaler(File file, int newWidth) throws IOException {
		InputStream is = null;
		try {
			is = new FileInputStream(file);
			Size size = getRoughSize(is, newWidth);
			try {
				is = new FileInputStream(file);
				roughScaleImage(is, size);
				scaleImage(newWidth);
			} finally {
				is.close();
			}
		} finally {
			is.close();
		}
	}

	BitmapScaler(AssetManager manager, String assetName, int newWidth)
			throws IOException {
		InputStream is = null;
		try {
			is = manager.open(assetName);
			Size size = getRoughSize(is, newWidth);
			try {
				is = manager.open(assetName);
				roughScaleImage(is, size);
				scaleImage(newWidth);
			} finally {
				is.close();
			}
		} finally {
			is.close();
		}
	}

	Bitmap getScaled() {
		return scaled;
	}

	private void scaleImage(int newWidth) {
		int width = scaled.getWidth();
		int height = scaled.getHeight();

		float scaleWidth = ((float) newWidth) / width;
		float ratio = ((float) scaled.getWidth()) / newWidth;
		int newHeight = (int) (height / ratio);
		float scaleHeight = ((float) newHeight) / height;

		Matrix matrix = new Matrix();
		matrix.postScale(scaleWidth, scaleHeight);

		scaled = Bitmap.createBitmap(scaled, 0, 0, width, height, matrix, true);
	}

	private void roughScaleImage(InputStream is, Size size) {
		Matrix matrix = new Matrix();
		matrix.postScale(size.scale, size.scale);

		BitmapFactory.Options scaledOpts = new BitmapFactory.Options();
		scaledOpts.inSampleSize = size.sample;
		scaled = BitmapFactory.decodeStream(is, null, scaledOpts);
	}

	private void roughScaleImage(Resources resources, int resId, Size size) {
		Matrix matrix = new Matrix();
		matrix.postScale(size.scale, size.scale);

		BitmapFactory.Options scaledOpts = new BitmapFactory.Options();
		scaledOpts.inSampleSize = size.sample;
		scaled = BitmapFactory.decodeResource(resources, resId, scaledOpts);
	}

	private Size getRoughSize(InputStream is, int newWidth) {
		BitmapFactory.Options o = new BitmapFactory.Options();
		o.inJustDecodeBounds = true;
		BitmapFactory.decodeStream(is, null, o);

		Size size = getRoughSize(o.outWidth, o.outHeight, newWidth);
		return size;
	}

	private Size getRoughSize(Resources resources, int resId, int newWidth) {
		BitmapFactory.Options o = new BitmapFactory.Options();
		o.inJustDecodeBounds = true;
		BitmapFactory.decodeResource(resources, resId, o);

		Size size = getRoughSize(o.outWidth, o.outHeight, newWidth);
		return size;
	}

	private Size getRoughSize(int outWidth, int outHeight, int newWidth) {
		Size size = new Size();
		size.scale = outWidth / newWidth;
		size.sample = 1;

		int width = outWidth;
		int height = outHeight;

		int newHeight = (int) (outHeight / size.scale);

		while (true) {
			if (width / 2 < newWidth || height / 2 < newHeight) {
				break;
			}
			width /= 2;
			height /= 2;
			size.sample *= 2;
		}
		return size;
	}
}

Or, try out the Eclipse test project.


Android Shortcuts

2011/01/23

Shortcuts are an often overlooked usability enhancer for your Android app.  They are a nice alternative to providing quick access to particular locations or views of data, where you would otherwise have “favorites” or “most recently used” function. I wanted to take a moment to share my implementation experience.

My use case: I am the author of the Clear Sky Droid (and the donate version) app which is available on the Android market. Being a tad esoteric, it’s probably not the best example, but I’ll try my best to explain the usability problem in abstract terms. Essentially, the user can define a list of favorites by searching for items. From the list of favorites, they can then select a detailed view of the item. The usability problem is that many users want direct access to the detailed view of a particular favorite – something that is normally 3 clicks away.

Shortcuts to the rescue. With this feature, the user can set up a desktop shortcut for direct access to the detailed view of a particular favorite. It’s now one click away.

What exactly is a shortcut? It’s an Android desktop artifact. It is not part of your application. When clicked, it broadcasts an intent that you have defined. So you can see where this is going. You simply need to set up your application to respond appropriately to whatever intent is configured into the shortcut.

First we will register an activity into the Android “create shortcuts” home screen menu. Create a new activity, or add the functionality to an existing activity. Respond to the create shortcut intent,

<intent-filter>
    <action android:name="android.intent.action.CREATE_SHORTCUT" />
    <category android:name="android.intent.category.DEFAULT" />
</intent-filter>

You can look at CSDroid’s AndroidManifest.xml to see this in context.

This is all it takes to make your application show up when the user long presses on the home screen and select “shortcuts”.

You must now make your application handle the intent. In CSDroid, look at the startService() method in the class TabwidgetActivity, find the line where it’s checking if the intent’s action is “android.intent.action.CREATE_SHORTCUT”. This means the user has long pressed the home screen and selected your app.

Gather any information necessary to customize the shortcut to access to a particular aspect of your application.  For CSDroid, we open a dialog that lets the user select a favorite item. Looking at the code in TabwidgetActivity mentioned above, you can see that a dialog  is shown when the CREATE_SHORTCUT intent is received.  The dialog builder class is FaveShortcutDialogBuilder.

FaveShortcutDialogBuilder just gets the favorite item then calls back to TabwidgetActivity‘s saveShortcut() method. This is where we request creation of the desktop shortcut. Here are the steps,

  1. Create an intent that targets your application, setting action, data, extras, etc accordingly to perform the specific action requested when the user clicks the shortcut
  2. Wrap that intent in another intent that will be sent back to the Android “create shortcut” activity
  3. Set this wrapping intent as the result of your activity, and finish

Here’s the code from CSDroid,

void saveShortcut(Site site) {
    Intent shortcutIntent = new Intent(this, TabWidgetActivity.class);
    shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
    shortcutIntent.putExtra("org.jtb.csdroid.site.id", site.getId());

    Intent intent = new Intent();
    intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
    intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, site.getName());
    intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(this, R.drawable.icon));
    intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
    setResult(RESULT_OK, intent);
    finish();
}

In this case, if CSDroid sees a site ID in the intent’s extras, it will perform the shortcut action for that site ID.

I should not that in the context of CSDroid the code could be organized better. A better pattern would separate the main activity from the short cut creation, and shortcut handling activities. So you might have MainActivity, CreateShortcutActivity, and HandleShortcutActivity.

One last gotcha … my first idea was to auto-create the shortcut from the app. In other words, circumvent the steps where the user must long-press the home screen, select shortcuts, select my app, then select a favorite – all from within the application. CSDroid already has an activity that lists the user’s favorites. I wanted to simply add a “add shortcut” option right there. Well, you can’t do that. You cannot programmatically create shortcuts. Only the user can do that.

CSDroid is open source. Visit the CSDroid Google code project page to find the source code, etc.

And finally, all due credit to Attilla Danko, the owner of ClearDarkSky.com, the data source for this application.