Custom View to draw, rotate, erase and convert images to black and white.

Print Friendly

This article describes how to create a reusable class that can upload images from our Gallery or take a picture, put them on a canvas and move them around the screen, resize them, erase them and convert them to black and white. This tutorial uses a class that extends View and uses onTouchEvents. This is class also takes care of the VM Budget issue of loading images from the gallery by optimizing the memory each image uses. It uses android 2.2 and above.

Before buying this article, you can download the apk file here: http://ece301-examples.googlecode.com/files/CanvasView.apk

This is a class I created as part of an application I am working on where I need users to select images and put them onto the screen, move them around and resize them. So lets get started.


First, we create a project “CanvasView”. The package name is com.canvas.view.library. We create two classes; Stye.java and TouchView.java. Style.java will be the activity.

The main.xml will contain the following code:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical" android:layout_width="fill_parent"
	android:layout_height="fill_parent" android:id="@+id/style_layout"
	android:background="#2be5e7">
	<Button android:layout_width="wrap_content"
		android:layout_height="wrap_content" android:text="Picture"
		android:id="@+id/camera_button" android:layout_alignParentBottom="true"></Button>
	<Button android:layout_width="wrap_content"
		android:layout_height="wrap_content" android:text="Image to BW"
		android:id="@+id/bw_button" android:layout_toRightOf="@id/camera_button"
		android:layout_alignParentBottom="true"></Button>
	<Button android:layout_width="wrap_content"
		android:layout_height="wrap_content" android:text="Trash" android:id="@+id/trash_button"
		android:layout_toRightOf="@id/bw_button"
		android:layout_alignParentBottom="true"></Button>
</RelativeLayout>

Now we will start modifying TouchView. TouchView will extend View, thus allowing us to create custom views that we can draw on and do different things. First, I will show the variables that will be used and the constructor.

public class TouchView extends View {

	private static final int INVALID_POINTER_ID = -1;

	private final String TAG = "TESTESTEST";

	private Drawable mImage;
	
	public void setmImage(Drawable image){
		mImage = image;
	}
	// width and height of original image
	private float mImageWidth;
	private float mImageHeight;
	
	// when image is scaled, we use this to calculate the bounds of the image
	private int mImageWidthScaled;
	private int mImageHeightScaled;

	public float mPosX = 150;
	public float mPosY = 300;

	public float getmPosX() {
		return mPosX;
	}

	public void setmPosX(float mPosX) {
		this.mPosX = mPosX;
	}

	public float getmPosY() {
		return mPosY;
	}

	public void setmPosY(float mPosY) {
		this.mPosY = mPosY;
	}

	private float mLastTouchX;
	private float mLastTouchY;

	private Paint mBorderLeftLine;
	private Paint mBorderTopLine;
	private Paint mBorderRightLine;
	private Paint mBorderBottomLine;

	private int mActivePointerId = INVALID_POINTER_ID;
	private ScaleGestureDetector mScaleDetector;
	private float mScaleFactor = 1f;

	// this is to tell Style what view number I am in the array.
	private int mNumberView;
	
	// this is what draws the red line around the TouchView to tell the user
	// this one is currently selected
	private boolean mSelected = false;


	public void setmSelected(boolean mSelected) {
		this.mSelected = mSelected;
	}

	private Style mStyle;
	
	// this is to keep a reference to the original image, so when we ship, we can tar
	// the images that are needed.
	private String mUri;
	
	public void setImageLocation(String path){
		this.mUri = path;
	}

	public TouchView(Context context,Style style, BitmapDrawable image, int count, float scaleFactor){
		super(context);
		this.mImage = image;
		mImageWidth = image.getBitmap().getWidth();
		mImageHeight = image.getBitmap().getHeight();
		mImageWidthScaled = (int) (mImageWidth*scaleFactor);
		mImageHeightScaled = (int) (mImageHeight*scaleFactor);
		this.mNumberView = count;
		this.mStyle = style;
		this.mScaleFactor = scaleFactor;
		init(context);
	}

Lets only focus on the constructor right now. The constructor passes a Drawable which will be the image for this view. The imageWidth and Height must be defined in the constructor. What these values will do is give me the actual image width and height used in the view. Later we will use these values for when we have different views and the user clicks on individual ones, that view consumes the onTouchView event. Intrinsic Width and height did not work well for me.

The mNumberView will define which View I am. In Style.java I will have an ArrayList with the list of views. This is how I manage the views.

mScaleFactor is the scale factor for zooming in and out the image.

Next piece of code initializes values for drawing to happen when onDraw() is called.

private void init(Context context) {
		mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
		mBorderLeftLine = new Paint();
		mBorderRightLine = new Paint();
		mBorderBottomLine = new Paint();
		mBorderTopLine = new Paint();
		setBorderParams(Color.RED,2);

	}

	private void setBorderParams(int color, float width) {
		mBorderLeftLine.setColor(color);
		mBorderLeftLine.setStrokeWidth(width);
		mBorderRightLine.setColor(color);
		mBorderRightLine.setStrokeWidth(width);
		mBorderBottomLine.setColor(color);
		mBorderBottomLine.setStrokeWidth(width);
		mBorderTopLine.setColor(color);
		mBorderTopLine.setStrokeWidth(width);
		mImage.setBounds(0, 0,mImage.getIntrinsicWidth(),mImage.getIntrinsicHeight());

	}

	public void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		canvas.save();
		canvas.translate(mPosX, mPosY);
		canvas.scale(mScaleFactor, mScaleFactor);
		mImage.draw(canvas);
		if (mSelected){
			canvas.drawLine(0,
					0,
					mImage.getIntrinsicWidth(),
					0,
					mBorderTopLine);
			canvas.drawLine(0, mImage.getIntrinsicHeight(),
					mImage.getIntrinsicWidth(),
					mImage.getIntrinsicHeight(),
					mBorderBottomLine);
			canvas.drawLine(0,
					0,
					0, 
					mImage.getIntrinsicHeight(),
					mBorderLeftLine);
			canvas.drawLine(mImage.getIntrinsicWidth(),
					0,
					mImage.getIntrinsicWidth(),
					mImage.getIntrinsicHeight(),
					mBorderRightLine);
		}
		canvas.restore();
	}

The constructor calls the method init(context); Because I am drawing a red rectangle around the image that is currently in use by a user, I create Paints for each line and setBorderParams(); sets the color and stroke width of those lines. ALSO, very important is mImage.setBounds(); If I do not do this, the image (Drawable) will not draw when onDraw() is called.

onDraw() method simply calls canvas.translate() to move the image around the screen by using mPosX and mPosY values. These values are set in my onTouchEvent and we will see how later.
OnDraw() also uses canvas.scale() to scale the image depending when a user uses multitouch to stretch the image.
OnDraw() also draws the a red rectangle around the currently selected image by a user. This is done with a boolean, mSelected.

I think so far, things are simple. But the difficult part of this class is the onTouchEvent method. onTouchEvent is used for the following:

1. Find out when the user does multitouch.
2. Move the image around by setting the values mPosX and mPosY
3. Check if this view is the one being selected because we can have more than 1 of these views on the screen at any time. (In the picture for this article I have 4. Each image represents an object of this view).

public boolean onTouchEvent(MotionEvent event) {
		//ImageView view = (ImageView) v;
		mScaleDetector.onTouchEvent(event);
		boolean intercept = false;
		//boolean defaultResult = onTouchEvent(event);

		switch (event.getAction() & MotionEvent.ACTION_MASK) {

		case MotionEvent.ACTION_DOWN:
			mLastTouchX = event.getX();
			mLastTouchY = event.getY();
			mActivePointerId = event.getPointerId(0);
			// this should mean that I accept the touch out of all my views
			/*if (((mLastTouchX >= mPosX) && (mLastTouchX <= mPosX+ mImage.getIntrinsicWidth())
					&& (mLastTouchY >= mPosY) && (mLastTouchY <= mPosY + mImage.getIntrinsicHeight())))*/
			if (((mLastTouchX >= mPosX) && (mLastTouchX <= mPosX+ mImageWidthScaled)
					&& (mLastTouchY >= mPosY) && (mLastTouchY <= mPosY + mImageHeightScaled))){
				Log.i(TAG,"My view is here: "+mNumberView);
				intercept = true;
				mSelected = true;
				mStyle.setmCurrentView(mNumberView);

			}

			Log.i(TAG,"Action down");
			Log.i(TAG,"x is: "+mLastTouchX);
			Log.i(TAG,"y is: "+mLastTouchY);
			break;
		case MotionEvent.ACTION_UP:
			setFocusable(false);
			mImageWidthScaled = (int) (mImageWidth*mScaleFactor);
			mImageHeightScaled = (int) (mImageHeight*mScaleFactor);
			/*			mPosX = (int) event.getX();
			mPosY = (int) event.getY();*/
			mActivePointerId = INVALID_POINTER_ID;
			// stop the red rectangle from being drawn around the View
			mSelected = false;
			break;
		case MotionEvent.ACTION_CANCEL: 
			mActivePointerId = INVALID_POINTER_ID;
			// stop the red rectangle from being drawn around the View
			mSelected = false;
			break;
		case MotionEvent.ACTION_MOVE:
			final int pointerIndex = event.findPointerIndex(mActivePointerId);
			final float x = event.getX(pointerIndex);
			final float y = event.getY(pointerIndex);
			if (!mScaleDetector.isInProgress()) {
				final float dx = x - mLastTouchX;
				final float dy = y - mLastTouchY;

				mPosX += dx;
				mPosY += dy;

				// Invalidate to request a redraw
				invalidate();
			}
			else{
				Log.i(TAG,"Now scaling is happening");
			}
			// Remember this touch position for the next move event
			mLastTouchX = x;
			mLastTouchY = y;

			break;
		case MotionEvent.ACTION_POINTER_UP:
			final int pointerIndex2 = (event.getAction() & MotionEvent.ACTION_POINTER_ID_MASK) 
			>> MotionEvent.ACTION_POINTER_ID_SHIFT;
					final int pointerId = event.getPointerId(pointerIndex2);
					if (pointerId == mActivePointerId) {
						// This was our active pointer going up. Choose a new
						// active pointer and adjust accordingly.
						final int newPointerIndex = pointerIndex2 == 0 ? 1 : 0;
						mLastTouchX = event.getX(newPointerIndex);
						mLastTouchY = event.getY(newPointerIndex);
						mActivePointerId = event.getPointerId(newPointerIndex);
					}
					break;
					default:
						//return defaultResult;
		}
		return intercept;
	}

When first doing an ACION_DOWN I get the pixel values where the user is touching. The if statement is checking if these pixel values (x,y) are inside the image location. So my image is at position (100,200) with a width of 50 and height of 50, then it is checking if the touch falls within those bounds, if it does, then I set the boolean intercept to true, else to false. See that at the end we return intercept. This means that if onTouchEvent returns false, this view does not consume the touch and is passed to other views. Also mActivePointerId = event.getPointerId(0); is getting the pointerId for multitouch events.

Here is the code again for the case ACTION_DOWN:

case MotionEvent.ACTION_DOWN:
			mLastTouchX = event.getX();
			mLastTouchY = event.getY();
			mActivePointerId = event.getPointerId(0);
			if (((mLastTouchX >= mPosX) && (mLastTouchX <= mPosX+ mImageWidthScaled)
					&& (mLastTouchY >= mPosY) && (mLastTouchY <= mPosY + mImageHeightScaled))){
				Log.i(TAG,"My view is here: "+mNumberView);
				intercept = true;
				mSelected = true;
				mStyle.setmCurrentView(mNumberView);

			}

			Log.i(TAG,"Action down");
			Log.i(TAG,"x is: "+mLastTouchX);
			Log.i(TAG,"y is: "+mLastTouchY);
			break;

Now on ACTION_UP recalculates the width and height of the image in case it was stretched. It also makes the view unfocused. This is so that when we try to click on another object, the program doesn’t think we are still focused on this object and moves this one instead. Here it is again:

case MotionEvent.ACTION_UP:
			setFocusable(false);
			mImageWidthScaled = (int) (mImageWidth*mScaleFactor);
			mImageHeightScaled = (int) (mImageHeight*mScaleFactor);
			/*			mPosX = (int) event.getX();
			mPosY = (int) event.getY();*/
			mActivePointerId = INVALID_POINTER_ID;
			// stop the red rectangle from being drawn around the View
			mSelected = false;
			break;

ACTION_CANCEL doesn’t really do anything so I won’t go over it.

ACTION_MOVE check that currently the view is not being scaled (stretched) and if it is not, then it moves the image around the screen.

case MotionEvent.ACTION_MOVE:
			final int pointerIndex = event.findPointerIndex(mActivePointerId);
			final float x = event.getX(pointerIndex);
			final float y = event.getY(pointerIndex);
			if (!mScaleDetector.isInProgress()) {
				final float dx = x - mLastTouchX;
				final float dy = y - mLastTouchY;

				mPosX += dx;
				mPosY += dy;

				// Invalidate to request a redraw
				invalidate();
			}
			else{
				Log.i(TAG,"Now scaling is happening");
			}
			// Remember this touch position for the next move event
			mLastTouchX = x;
			mLastTouchY = y;

			break;

Finally POINTER_UP is used for multitouch events and mainly here for stretching the image when a user uses multitouch.

case MotionEvent.ACTION_POINTER_UP:
			final int pointerIndex2 = (event.getAction() & MotionEvent.ACTION_POINTER_ID_MASK) 
			>> MotionEvent.ACTION_POINTER_ID_SHIFT;
					final int pointerId = event.getPointerId(pointerIndex2);
					if (pointerId == mActivePointerId) {
						// This was our active pointer going up. Choose a new
						// active pointer and adjust accordingly.
						final int newPointerIndex = pointerIndex2 == 0 ? 1 : 0;
						mLastTouchX = event.getX(newPointerIndex);
						mLastTouchY = event.getY(newPointerIndex);
						mActivePointerId = event.getPointerId(newPointerIndex);
					}
					break;

Now in order to do multitouch and be able to scale the image, we have to create a class inside this class. It extends ScaleGestureDetector and this is the reason why we need to use android 2.2.

private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
		@Override
		public boolean onScale(ScaleGestureDetector detector) {
			mScaleFactor *= detector.getScaleFactor();

			// Don't let the object get too small or too large.
			mScaleFactor = Math.max(0.05f, Math.min(mScaleFactor, 2.0f));
			invalidate();
			Log.i(TAG,"New Image size: widht: "+mImage.getIntrinsicWidth()+" height: "+mImage.getIntrinsicHeight());
			return true;
		}
	}

All it does it when we detect that scaling is happening, it calculates the scale factor. in the onDraw method we have canvas.scale(). This is where this value comes in and scales the image accordingly.

Finally as an added bonus, I do some image processing to turn the image from color to black and white

public void greyScaler() {    
		ColorMatrix cm = new ColorMatrix();
		cm.set(new float[] {
				.21f, .71f, .07f, 0, 0, 
				.21f, .71f, .07f, 0, 0, 
				.21f, .71f, .07f, 0, 0, 
				0, 0, 0, 1, 0 });
		mImage.setColorFilter(new ColorMatrixColorFilter(cm));
		invalidate();
	}

It took me a while to figure it out since I never worked with ColorMatrix before. The array you see is a multiplier. It comes from here: http://developer.android.com/reference/android/graphics/ColorMatrix.html

Basically the RGB colors + Alpha are multiplied by the matrix values above. One algorithm for black and white is to as follows .21*R + .71*G + .07B. This is what I have above. invalidate is called every time we want the onDraw() method to be called manually by us. In my case, after changing the image filter, it will not update until onDraw is called, thus I call it.

If you are following me so far, we are not done yet.We have created the Object that will take care of all the work for us to move the image, scale it, etc. This class can be extended and other methods added or you can add other methods yourself that you might need. Like some extra image processing, rotation, etc.

Style.java extends Activity and holds an ArrayList of type TouchView to keep track of our object creation. Let’s start with the variables and onCreate method:

public class Style extends Activity {

	private final static String TAG = "TESTESTESTEST";

	// to take a picture
	private static final int CAMERA_PIC_REQUEST = 1111;
	private static final int GALLERY_PIC_REQUEST = 1112;

	private Button mCameraButton;
	private Button mBwButton;
	private Button mTrashButton;

	// current view is the current selected view - hopefully this will work ok
	private int mCurrentView = 0;

	public int getmCurrentView() {
		return mCurrentView;
	}

	public void setmCurrentView(int mCurrentView) {
		this.mCurrentView = mCurrentView;
	}

	// this tells me how many views I currently have.
	private int mViewsCount = 0;

	private ArrayList<View> mViewsArray = new ArrayList<View>();

	private static Style mStyle;

	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		requestWindowFeature(Window.FEATURE_NO_TITLE);
		getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 
				WindowManager.LayoutParams.FLAG_FULLSCREEN);
		setContentView(R.layout.main);

		mStyle = this;

		mTrashButton = (Button) findViewById(R.id.trash_button);
		mTrashButton.setClickable(true);
		mTrashButton.setOnClickListener(new View.OnClickListener() {

			@Override
			public void onClick(View v) {
				Log.i(TAG,"Trash clicked");
				Log.i(TAG,"Array size is: "+mViewsArray.size());
				if (mViewsArray.size() > 0){
					Log.i(TAG,"Should remove this view");
					RelativeLayout layout = (RelativeLayout) findViewById(R.id.style_layout);
					layout.removeView(mViewsArray.get(mCurrentView));
					mViewsArray.remove(mCurrentView);
					mViewsCount -=1;
				}

			}
		});

		mCameraButton = (Button) findViewById(R.id.camera_button);
		mCameraButton.setClickable(true);
		mCameraButton.setOnClickListener(new View.OnClickListener() {

			@Override
			public void onClick(View v) {
				AlertDialog.Builder builder = new AlertDialog.Builder(v.getContext());
				builder.setTitle("Select:");
				final CharSequence[] chars = {"Take Picture", "Choose from Gallery"};
				builder.setItems(chars, new android.content.DialogInterface.OnClickListener(){

					@Override
					public void onClick(DialogInterface dialog, int which) {
						if(which == 0){
							Intent cameraIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
							startActivityForResult(cameraIntent, CAMERA_PIC_REQUEST);
						}else
							if(which == 1){
								Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI);
								startActivityForResult(intent,GALLERY_PIC_REQUEST);
							}
						dialog.dismiss();
					}

				}
				);
				builder.show();
			}
		});

		mBwButton = (Button) findViewById(R.id.bw_button);
		mBwButton.setClickable(true);
		mBwButton.setOnClickListener(new View.OnClickListener() {

			@Override
			public void onClick(View v) {
				if (mViewsArray.size() > 0){
					((TouchView) mViewsArray.get(mCurrentView)).greyScaler();
				}
				else{
					Toast.makeText(v.getContext(), "Please select an image first before using this function.", Toast.LENGTH_SHORT).show();
				}
			}
		});
	}

The onCreate method holds my click listeners for my buttons to erase, take photo and make black and white.

The trash button removes the view from both the layout and then ArrayList, mViewsArray. Here it is again:

mTrashButton.setOnClickListener(new View.OnClickListener() {

			@Override
			public void onClick(View v) {
				Log.i(TAG,"Trash clicked");
				Log.i(TAG,"Array size is: "+mViewsArray.size());
				if (mViewsArray.size() > 0){
					Log.i(TAG,"Should remove this view");
					RelativeLayout layout = (RelativeLayout) findViewById(R.id.style_layout);
					layout.removeView(mViewsArray.get(mCurrentView));
					mViewsArray.remove(mCurrentView);
					mViewsCount -=1;
				}

			}
		});

mViewsCount keeps a count of TouchViews created.

mCameraButton creates an Alert to let a user choose between using a current image from the Gallery or take a picture. Here it is again:

mCameraButton.setOnClickListener(new View.OnClickListener() {

			@Override
			public void onClick(View v) {
				AlertDialog.Builder builder = new AlertDialog.Builder(v.getContext());
				builder.setTitle("Select:");
				final CharSequence[] chars = {"Take Picture", "Choose from Gallery"};
				builder.setItems(chars, new android.content.DialogInterface.OnClickListener(){

					@Override
					public void onClick(DialogInterface dialog, int which) {
						if(which == 0){
							Intent cameraIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
							startActivityForResult(cameraIntent, CAMERA_PIC_REQUEST);
						}else
							if(which == 1){
								Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI);
								startActivityForResult(intent,GALLERY_PIC_REQUEST);
							}
						dialog.dismiss();
					}

				}
				);
				builder.show();
			}
		});

It also uses android actions: android.provider.MediaStore.ACTION_IMAGE_CAPTURE and Intent.ACTION_PICK with android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI.

mBwButton calls the greyScaler() method in the TouchView to make the image black and white.

When creating these classes, I thought it would be useful to not only load the images from the gallery or from taking a picture, but also to keep track of its path. However, when we call startActivityForResult(). We do not get the absolute path, we I needed to add this function so I could get it.

[soucecode language="java"]
public String getPath(Uri uri){
String[] filePathColumn={MediaStore.Images.Media.DATA};

Cursor cursor=getContentResolver().query(uri, filePathColumn, null, null, null);
cursor.moveToFirst();
int columnIndex=cursor.getColumnIndex(filePathColumn[0]);
Log.i(TAG,”Image path is: “+cursor.getString(columnIndex));
return cursor.getString(columnIndex);
}
[/sourcecode]

Finally, the last thing we need to do here is onActivityResult();

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		// usedView is a bool that checks is a view was destroyed and this was reused.
		// if it wasn't reused, this means we create a new one.
		if (requestCode == CAMERA_PIC_REQUEST) {
			try{
				Uri selectedImage = data.getData();
				getPath(selectedImage);
				InputStream is;
				is = getContentResolver().openInputStream(selectedImage);
				BitmapFactory.Options opts = new BitmapFactory.Options();
				opts.inJustDecodeBounds = true;
				//BitmapFactory.decodeStream(bis,null,opts);
				BitmapFactory.decodeStream(is,null,opts);

				//The new size we want to scale to
				final int REQUIRED_SIZE=200;

				//Find the correct scale value. It should be the power of 2.
				int scale=1;
				while(opts.outWidth/scale/2>=REQUIRED_SIZE || opts.outHeight/scale/2>=REQUIRED_SIZE)
					scale*=2;

				Log.i(TAG,"Scale is: "+scale);
				opts.inSampleSize = scale;
				opts.inJustDecodeBounds = false;
				is = null;
				System.gc();
				InputStream is2 = getContentResolver().openInputStream(selectedImage);

				Bitmap returnedImage = BitmapFactory.decodeStream(is2, null, opts);
				Log.i(TAG,"Image width from bitmap: "+returnedImage.getWidth());
				Log.i(TAG,"Image height from bitmap: "+returnedImage.getHeight());
				Log.i(TAG,"Creating another View");
				TouchView newView = new TouchView(this,mStyle,new BitmapDrawable(returnedImage),mViewsCount,1f);
				newView.setImageLocation(getPath(selectedImage));
				newView.setClickable(true);
				// below is to ensure red border is drawn on new selected image
				newView.setmSelected(true);
				mViewsArray.add(newView);
				RelativeLayout layout = (RelativeLayout) findViewById(R.id.style_layout);
				layout.addView(mViewsArray.get(mViewsCount));
				newView.invalidate();
				mViewsCount+=1;
			}
			catch(NullPointerException e){
				//Do nothing
			} catch (FileNotFoundException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		if (requestCode == GALLERY_PIC_REQUEST){
			try {
				Uri selectedImage = data.getData();
				getPath(selectedImage);
				InputStream is;
				is = getContentResolver().openInputStream(selectedImage);
				BitmapFactory.Options opts = new BitmapFactory.Options();
				opts.inJustDecodeBounds = true;
				//BitmapFactory.decodeStream(bis,null,opts);
				BitmapFactory.decodeStream(is,null,opts);

				//The new size we want to scale to
				final int REQUIRED_SIZE=200;

				//Find the correct scale value. It should be the power of 2.
				int scale=1;
				while(opts.outWidth/scale/2>=REQUIRED_SIZE || opts.outHeight/scale/2>=REQUIRED_SIZE)
					scale*=2;

				Log.i(TAG,"Scale is: "+scale);
				opts.inSampleSize = scale;
				opts.inJustDecodeBounds = false;
				is = null;
				System.gc();
				InputStream is2 = getContentResolver().openInputStream(selectedImage);

				Bitmap returnedImage = BitmapFactory.decodeStream(is2, null, opts);
				Log.i(TAG,"Image width from bitmap: "+returnedImage.getWidth());
				Log.i(TAG,"Image height from bitmap: "+returnedImage.getHeight());
				Log.i(TAG,"Creating another View");
				TouchView newView = new TouchView(this,mStyle,new BitmapDrawable(returnedImage),mViewsCount,1f);
				newView.setImageLocation(getPath(selectedImage));
				newView.setClickable(true);
				// below is to ensure red border is drawn on new selected image
				newView.setmSelected(true);
				mViewsArray.add(newView);
				RelativeLayout layout = (RelativeLayout) findViewById(R.id.style_layout);
				layout.addView(mViewsArray.get(mViewsCount));
				newView.invalidate();
				mViewsCount+=1;
			} catch (FileNotFoundException e) {

			}
			catch (NullPointerException e){
			}
		}
	}

Now, this method is longer than it needs to be. Both GALLERY request and PIC REQUEST use the same algorithm. However, I decided to separate them just in case one of them needed something different than the other. Or if I wanted to change the size from grabbing images from Gallery vs taking the picture.

The algorithm is this:

Uri selectedImage = data.getData();
				getPath(selectedImage);
				InputStream is;
				is = getContentResolver().openInputStream(selectedImage);
				BitmapFactory.Options opts = new BitmapFactory.Options();
				opts.inJustDecodeBounds = true;
				//BitmapFactory.decodeStream(bis,null,opts);
				BitmapFactory.decodeStream(is,null,opts);

				//The new size we want to scale to
				final int REQUIRED_SIZE=200;

				//Find the correct scale value. It should be the power of 2.
				int scale=1;
				while(opts.outWidth/scale/2>=REQUIRED_SIZE || opts.outHeight/scale/2>=REQUIRED_SIZE)
					scale*=2;

				Log.i(TAG,"Scale is: "+scale);
				opts.inSampleSize = scale;
				opts.inJustDecodeBounds = false;
				is = null;
				System.gc();
				InputStream is2 = getContentResolver().openInputStream(selectedImage);

				Bitmap returnedImage = BitmapFactory.decodeStream(is2, null, opts);
				Log.i(TAG,"Image width from bitmap: "+returnedImage.getWidth());
				Log.i(TAG,"Image height from bitmap: "+returnedImage.getHeight());
				Log.i(TAG,"Creating another View");
				TouchView newView = new TouchView(this,mStyle,new BitmapDrawable(returnedImage),mViewsCount,1f);
				newView.setImageLocation(getPath(selectedImage));
				newView.setClickable(true);
				// below is to ensure red border is drawn on new selected image
				newView.setmSelected(true);
				mViewsArray.add(newView);
				RelativeLayout layout = (RelativeLayout) findViewById(R.id.style_layout);
				layout.addView(mViewsArray.get(mViewsCount));
				newView.invalidate();
				mViewsCount+=1;

First I get the Uri of the image.I quickly found that I was running out of memory after loading only 3 images because I was loading the images to memory and it was exceeding my VM Budget (they were 8 megapixel images). Following this: http://developer.android.com/training/displaying-bitmaps/load-bitmap.html

I created a scaled version of my images which greatly reduced the memory usage. You can read about it, but the main part for this was:

BitmapFactory.Options opts = new BitmapFactory.Options();
				opts.inJustDecodeBounds = true;
				//BitmapFactory.decodeStream(bis,null,opts);
				BitmapFactory.decodeStream(is,null,opts);

				//The new size we want to scale to
				final int REQUIRED_SIZE=200;

				//Find the correct scale value. It should be the power of 2.
				int scale=1;
				while(opts.outWidth/scale/2>=REQUIRED_SIZE || opts.outHeight/scale/2>=REQUIRED_SIZE)
					scale*=2;

				Log.i(TAG,"Scale is: "+scale);
				opts.inSampleSize = scale;
				opts.inJustDecodeBounds = false;
				is = null;
				System.gc();
				InputStream is2 = getContentResolver().openInputStream(selectedImage);

				Bitmap returnedImage = BitmapFactory.decodeStream(is2, null, opts);

I won’t go over the details as you can read about it on the link above, but this was a great optimization to my code.

Finally, Once I have the bitmap I use BitmapDrawable to pass a Drawable from a Bitmap to the constructor of TouchView along with other values. I then call my RelativeLayout and add the view to relative layout (vs in the mTrashButton removes the view from my relative layout). This all happens here:

Bitmap returnedImage = BitmapFactory.decodeStream(is2, null, opts);
				Log.i(TAG,"Image width from bitmap: "+returnedImage.getWidth());
				Log.i(TAG,"Image height from bitmap: "+returnedImage.getHeight());
				Log.i(TAG,"Creating another View");
				TouchView newView = new TouchView(this,mStyle,new BitmapDrawable(returnedImage),mViewsCount,1f);
				newView.setImageLocation(getPath(selectedImage));
				newView.setClickable(true);
				// below is to ensure red border is drawn on new selected image
				newView.setmSelected(true);
				mViewsArray.add(newView);
				RelativeLayout layout = (RelativeLayout) findViewById(R.id.style_layout);
				layout.addView(mViewsArray.get(mViewsCount));
				newView.invalidate();
				mViewsCount+=1;

And that is it! You now have a basic library to use in your applications. Should save you a lot of time.

Comments are closed.