Category Archives: Android

Android handling images and avoiding out of memory errors

Getting the dreaded “out of memory” looms when reading large images as bitmaps. The key is to scale the image to something smaller when reading the image. There are heaps of solutions on Stack Overflow. But here is some quick code:

 


         private Bitmap mBitmap;
	
	/**
	 * Get Bitmap image from the path.
	 * Scale image to RGB_565 and rotate image 90 degrees
	 */
	private void setBitmap(String path) {
		Uri uri = Uri.fromFile(new File(path));
		ContentResolver mContentResolver = getContentResolver();
		InputStream in = null;
		try {
			BitmapFactory.Options o = new BitmapFactory.Options();
			o.inPurgeable = true;
			o.inSampleSize = 1;
			o.inPreferredConfig = Bitmap.Config.RGB_565;
			in = mContentResolver.openInputStream(uri);
			mBitmap = BitmapFactory.decodeStream(in, null, o);
			mBitmap = rotateImage(mBitmap, 90);
			in.close();
		} catch (Exception e) {
			Log.e(TAG, "file " + path + " not found");
		}
	}
        
        public Bitmap rotateImage(Bitmap src, float degree) {
		// create new matrix
		Matrix matrix = new Matrix();
		// setup rotation degree
		matrix.postRotate(degree);
		Bitmap bmp = Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, true);
		return bmp;
	}

Now if you are doing testing here is the above approach but enables you to read the file from the “assets” folder.

         private void setBitmapFromAssets(String fileName) {
		try {
			AssetManager am = this.getAssets();
			InputStream in = am.open(fileName);
			BitmapFactory.Options o = new BitmapFactory.Options();
			o.inPurgeable = true;
			o.inSampleSize = 1;
			o.inPreferredConfig = Bitmap.Config.RGB_565;
			mBitmap = BitmapFactory.decodeStream(in, null, o);
			mBitmap = rotateImage(mBitmap, 90);
			in.close();
		} catch (Exception e) {
			Log.e(TAG, "file " + fileName + " not found");
		}
	}

Android enabling zooming and pan on images

Recently, while working on a project, my Android app required to display an image, use pinch zooming and use the double tap gesture to restore the image.

This functionality is quite similar to viewing an image in the Gallery. After much searching on Stack Overflow, I found the following solution and added the double tap gesture code to it.
The code to go into the Activity

Bitmap bm = BitmapFactory.decodeResource(this.getResources(),R.drawable.test_image);
TouchImageView touchImageView = new TouchImageView(this);
touchImageView.setImageBitmap(bm);
touchImageView.setMaxZoom(4f); 
setContentView(touchImageView);

The custom view which enhances the ImageView.

package com.terenceingram.testapplication.view;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.ImageView;

public class TouchImageView extends ImageView {

	Matrix matrix = new Matrix();

	// We can be in one of these 3 states
	static final int NONE = 0;
	static final int DRAG = 1;
	static final int ZOOM = 2;
	int mode = NONE;

	// Remember some things for zooming
	PointF last = new PointF();
	PointF start = new PointF();
	float minScale = 1f;
	float maxScale = 3f;
	float[] m;

	float redundantXSpace, redundantYSpace, origRedundantXSpace, origRedundantYSpace;;

	float width, height;
	static final int CLICK = 3;
	static final float SAVE_SCALE = 1f;
	float saveScale = SAVE_SCALE;

	float right, bottom, origWidth, origHeight, bmWidth, bmHeight, origScale, origBottom,origRight;

	ScaleGestureDetector mScaleDetector;
	GestureDetector mGestureDetector;

	Context context;

	public TouchImageView(Context context) {
		super(context);
		super.setClickable(true);
		this.context = context;
		mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());

		matrix.setTranslate(1f, 1f);
		m = new float[9];
		setImageMatrix(matrix);
		setScaleType(ScaleType.MATRIX);

		setOnTouchListener(new OnTouchListener() {

			@Override
			public boolean onTouch(View v, MotionEvent event) {

				boolean onDoubleTapEvent = mGestureDetector.onTouchEvent(event);
				if (onDoubleTapEvent) {
					// Reset Image to original scale values
					mode = NONE;
					bottom = origBottom;
					right = origRight;
					last = new PointF();
					start = new PointF();
					m = new float[9];
					saveScale = SAVE_SCALE;
					matrix = new Matrix();
					matrix.setScale(origScale, origScale);
					matrix.postTranslate(origRedundantXSpace, origRedundantYSpace);
					setImageMatrix(matrix);
					invalidate();
					return true;
				} 

				mScaleDetector.onTouchEvent(event);

				matrix.getValues(m);
				float x = m[Matrix.MTRANS_X];
				float y = m[Matrix.MTRANS_Y];
				PointF curr = new PointF(event.getX(), event.getY());

				switch (event.getAction()) {
				case MotionEvent.ACTION_DOWN:
					last.set(event.getX(), event.getY());
					start.set(last);
					mode = DRAG;
					break;
				case MotionEvent.ACTION_MOVE:
					if (mode == DRAG) {
						float deltaX = curr.x - last.x;
						float deltaY = curr.y - last.y;
						float scaleWidth = Math.round(origWidth * saveScale);
						float scaleHeight = Math.round(origHeight * saveScale);
						if (scaleWidth < width) { 							deltaX = 0; 							if (y + deltaY > 0)
								deltaY = -y;
							else if (y + deltaY < -bottom)
								deltaY = -(y + bottom);
						} else if (scaleHeight < height) { 							deltaY = 0; 							if (x + deltaX > 0)
								deltaX = -x;
							else if (x + deltaX < -right) 								deltaX = -(x + right); 						} else { 							if (x + deltaX > 0)
								deltaX = -x;
							else if (x + deltaX < -right) 								deltaX = -(x + right); 							if (y + deltaY > 0)
								deltaY = -y;
							else if (y + deltaY < -bottom)
								deltaY = -(y + bottom);
						}
						matrix.postTranslate(deltaX, deltaY);
						last.set(curr.x, curr.y);
					}
					break;

				case MotionEvent.ACTION_UP:
					mode = NONE;
					int xDiff = (int) Math.abs(curr.x - start.x);
					int yDiff = (int) Math.abs(curr.y - start.y);
					if (xDiff < CLICK && yDiff < CLICK) 						performClick(); 					break; 				case MotionEvent.ACTION_POINTER_UP: 					mode = NONE; 					break; 				} 				 				setImageMatrix(matrix); 				invalidate(); 				 				return true; // indicate event was handled 			} 		}); 		 		mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { 		    @Override 		    public boolean onDoubleTapEvent(MotionEvent e) { 		        return true; 		    } 		}); 	} 	@Override 	public void setImageBitmap(Bitmap bm) { 		super.setImageBitmap(bm); 		bmWidth = bm.getWidth(); 		bmHeight = bm.getHeight(); 	} 	public void setMaxZoom(float x) { 		maxScale = x; 	} 	private class ScaleListener extends 			ScaleGestureDetector.SimpleOnScaleGestureListener { 		@Override 		public boolean onScaleBegin(ScaleGestureDetector detector) { 			mode = ZOOM; 			return true; 		} 		@Override 		public boolean onScale(ScaleGestureDetector detector) { 			float mScaleFactor = (float) Math.min( 					Math.max(.95f, detector.getScaleFactor()), 1.05); 			float origScale = saveScale; 			saveScale *= mScaleFactor; 			if (saveScale > maxScale) {
				saveScale = maxScale;
				mScaleFactor = maxScale / origScale;
			} else if (saveScale < minScale) {
				saveScale = minScale;
				mScaleFactor = minScale / origScale;
			}
			right = width * saveScale - width
					- (2 * redundantXSpace * saveScale);
			bottom = height * saveScale - height
					- (2 * redundantYSpace * saveScale);
			if (origWidth * saveScale <= width
					|| origHeight * saveScale <= height) {
				matrix.postScale(mScaleFactor, mScaleFactor, width / 2,
						height / 2);
				if (mScaleFactor < 1) {
					matrix.getValues(m);
					float x = m[Matrix.MTRANS_X];
					float y = m[Matrix.MTRANS_Y];
					if (mScaleFactor < 1) {
						if (Math.round(origWidth * saveScale) < width) {
							if (y < -bottom) 								matrix.postTranslate(0, -(y + bottom)); 							else if (y > 0)
								matrix.postTranslate(0, -y);
						} else {
							if (x < -right) 								matrix.postTranslate(-(x + right), 0); 							else if (x > 0)
								matrix.postTranslate(-x, 0);
						}
					}
				}
			} else {
				matrix.postScale(mScaleFactor, mScaleFactor,
						detector.getFocusX(), detector.getFocusY());
				matrix.getValues(m);
				float x = m[Matrix.MTRANS_X];
				float y = m[Matrix.MTRANS_Y];
				if (mScaleFactor < 1) {
					if (x < -right) 						matrix.postTranslate(-(x + right), 0); 					else if (x > 0)
						matrix.postTranslate(-x, 0);
					if (y < -bottom) 						matrix.postTranslate(0, -(y + bottom)); 					else if (y > 0)
						matrix.postTranslate(0, -y);
				}
			}
			return true;

		}
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		width = MeasureSpec.getSize(widthMeasureSpec);
		height = MeasureSpec.getSize(heightMeasureSpec);
		// Fit to screen.
		float scale;
		float scaleX = (float) width / (float) bmWidth;
		float scaleY = (float) height / (float) bmHeight;
		scale = Math.min(scaleX, scaleY);
		matrix.setScale(scale, scale);
		setImageMatrix(matrix);
		saveScale = SAVE_SCALE;
		origScale = scale;

		// Center the image
		redundantYSpace = (float) height - (scale * (float) bmHeight);
		redundantXSpace = (float) width - (scale * (float) bmWidth);
		redundantYSpace /= (float) 2;
		redundantXSpace /= (float) 2;

		origRedundantXSpace = redundantXSpace;
		origRedundantYSpace = redundantYSpace;

		matrix.postTranslate(redundantXSpace, redundantYSpace);

		origWidth = width - 2 * redundantXSpace;
		origHeight = height - 2 * redundantYSpace;
		right = width * saveScale - width - (2 * redundantXSpace * saveScale);
		bottom = height * saveScale - height
				- (2 * redundantYSpace * saveScale);
		origRight = right;
		origBottom = bottom;
		setImageMatrix(matrix);
	}

}

Martin Fowler has an interesting presentation on the development of mobile applications and conundrum of the various platforms.

I like this quote from Giles Alexander from Thoughtworks:

“Mobile is the new web: in 2000 businesses were realizing that the future of commerce and customer relations was on the just-exploding web. In a few years time, web commerce had eclipsed traditional off-line commerce. Mobile commerce is currently a fraction of web commerce. But in a few years time it will, in turn, eclipse traditional web commerce.”

What struck me was slide 17 that stated: “The choice between native apps and webapp is essentially between user-experience and affordability”

It will be interesting to see what develops as iOs and Android dominate the market, will Windows make inroads. What about Blackberry? Then there is also the web. How does that all fit in? I guess the consumer will decide.

As a developer, where do I put my skills and learning?

My first Android App

After much time wasting I finally decided to give writing my first android app.

So I went to the official source: http://developer.android.com/training/basics/firstapp. After downloading and installing the various bits and pieces, I followed the instructions and was delighted to see “Hello World” running on my Galaxy S2. Stupidly I couldn’t get the app to do exactly what the tutorial wanted. Maybe it’s just late.

My first impressions of looking at the code in Eclipse. It’s very Java’ery. I guess after well over a decade of developing webapps doing the layout of my lame widget seemed foreign. One surprise was a whole heap of XML. Sigh! I thought the powers that be had slayed that beast.

I ordered Professional Android 4 Development from , it has a cool looking Terminator on the cover. Hopefully this book ins’t crap.