Creating a Simple Custom View in Android

Recently I needed to create an area on an Android screen that a user could scribble their signature on. Custom views are the ideal way to do this, they are simple to implement and make for good reusable code. This example application will consist of just 2 classes, the SignatureViewTutorialActivity class and the SignatureView class.

Here is a quick and dirty explanation of the code:

mBitmap and mCanvas are linked and are essentially an offscreen buffer that we write to
mPath holds all our signature strokes
mBitmapPaint just holds the dither style for writing our bitmap
mPaint holds our drawing styles for our strokes
On touch_move we update mPath with our strokes
On touch_up we draw our strokes to the offscreen buffer(mCanvas) and clear our strokes (mPath)
onDraw - draws the offscreen buffer(mBitmap) to the visible canvas and also any current strokes (mPath)

First let’s start with the SignatureView class, this is our custom view and so needs to inherit the View class so we start with:

 
public class SignatureView extends View {
	private Bitmap  mBitmap;
        private Canvas  mCanvas;
        private Path    mPath;
        private Paint   mBitmapPaint;
        private Paint   mPaint;

And then we need to set up the constructors…

//3 Constructors, covers instantiating manually or from a layout file
public SignatureView(Context context) {
	super(context);
	initSignatureView();
}
 
public SignatureView(Context context,AttributeSet attrs) {
	super(context,attrs);
	initSignatureView();
}
 
public SignatureView(Context context,AttributeSet attrs,int defaultStyle) {
	super(context,attrs,defaultStyle);
	initSignatureView();
}

This function does some initialisation:

protected void initSignatureView(){
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setColor(Color.BLACK);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeWidth(12);
}

Now we need to override the onDraw method to make it bend to our will. getMeasuredHeight() and getMeasuredWidth() return the size of the view so we can use this to create a bitmap of the same size. This makes the SignatureView nice and flexible:

@Override
public void onDraw(Canvas canvas) {
	 if(mBitmap == null)
	 {
		 Log.d("SignatureView","Height: " + getMeasuredHeight() + "Width: " + getMeasuredWidth());
		 mBitmap = Bitmap.createBitmap( getMeasuredWidth(), getMeasuredHeight(),Bitmap.Config.ARGB_8888);
		 mBitmap.eraseColor(Color.WHITE);
		 mCanvas = new Canvas(mBitmap);//offscreen canvas
		 mPath = new Path();
		 mBitmapPaint = new Paint(Paint.DITHER_FLAG );
	 }
	 canvas.drawColor(Color.WHITE);
	 canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint); //draw offscreen changes
	 canvas.drawPath(mPath, mPaint); //draw current path
}

We also have to override the onTouchEvent method to handle user input:

 @Override
 public boolean onTouchEvent(MotionEvent event) {
     float x = event.getX();
     float y = event.getY();
 
     switch (event.getAction()) {
         case MotionEvent.ACTION_DOWN:
             touch_start(x, y);
             invalidate();
             break;
         case MotionEvent.ACTION_MOVE:
             touch_move(x, y);
             invalidate();
             break;
         case MotionEvent.ACTION_UP:
             touch_up();
             invalidate();
             break;
     }
     return true;
 }

Here are our last bits of code:

 
private float mX, mY;
private static final float TOUCH_TOLERANCE = 4;
 
 private void touch_start(float x, float y) {
     mPath.reset();
     mPath.moveTo(x, y);
     mX = x;
     mY = y;
 }
 private void touch_move(float x, float y) {
     float dx = Math.abs(x - mX);
     float dy = Math.abs(y - mY);
     if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
         mPath.quadTo(mX, mY, (x + mX)/2, (y + mY)/2);
         mX = x;
         mY = y;
     }
 }
 private void touch_up() {
     mPath.lineTo(mX, mY);
     mCanvas.drawPath(mPath, mPaint);// commit the path to our offscreen
     mPath.reset();// kill this so we don't double draw
 }

So that’s our custom view sorted, all we need to do now is use it! Very simple, all we need to do is create our SignatureViewTutorialActivity class and update our main.xml layout…

public class SignatureViewTutorialActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}

And our main.xml looks like this:

 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
 
    <com.platoevolved.signatureviewtutorial.SignatureView
        android:id="@+id/signatureView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</LinearLayout>

Here are the complete listings:
SignatureView.java

package com.platoevolved.signatureviewtutorial;
 
import android.content.Context;
import android.graphics.*;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
 
public class SignatureView extends View {
 
	private Bitmap  mBitmap;
        private Canvas  mCanvas;
        private Path    mPath;
        private Paint   mBitmapPaint;
        private Paint   mPaint;
 
    //3 Constructors, covers instantiating manually or from a layout file
        public SignatureView(Context context) {
		super(context);
		initSignatureView();
	}
 
	public SignatureView(Context context,AttributeSet attrs) {
		super(context,attrs);
		initSignatureView();
	}
 
	public SignatureView(Context context,AttributeSet attrs,int defaultStyle) {
		super(context,attrs,defaultStyle);
		initSignatureView();
	}
 
	protected void initSignatureView(){
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setColor(Color.BLACK);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeWidth(12);
	}
 
	 @Override
	public void onDraw(Canvas canvas) {
 
		 if(mBitmap == null)
		 {
			 Log.d("SignatureView","Height: " + getMeasuredHeight() + "Width: " + getMeasuredWidth());
			 mBitmap = Bitmap.createBitmap( getMeasuredWidth(), getMeasuredHeight(),Bitmap.Config.ARGB_8888);
			 mBitmap.eraseColor(Color.WHITE);
			 mCanvas = new Canvas(mBitmap);//offscreen canvas
			 mPath = new Path();
			 mBitmapPaint = new Paint(Paint.DITHER_FLAG );
		 }
		 canvas.drawColor(Color.WHITE);
		 canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint); //draw offscreen changes
		 canvas.drawPath(mPath, mPaint); //draw current path
	}
 
 
 
 @Override
 public boolean onTouchEvent(MotionEvent event) {
     float x = event.getX();
     float y = event.getY();
 
     switch (event.getAction()) {
         case MotionEvent.ACTION_DOWN:
             touch_start(x, y);
             invalidate();
             break;
         case MotionEvent.ACTION_MOVE:
             touch_move(x, y);
             invalidate();
             break;
         case MotionEvent.ACTION_UP:
             touch_up();
             invalidate();
             break;
     }
     return true;
 }
 
 
 private float mX, mY;
 private static final float TOUCH_TOLERANCE = 4;
 
 private void touch_start(float x, float y) {
     mPath.reset();
     mPath.moveTo(x, y);
     mX = x;
     mY = y;
 }
 private void touch_move(float x, float y) {
     float dx = Math.abs(x - mX);
     float dy = Math.abs(y - mY);
     if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
         mPath.quadTo(mX, mY, (x + mX)/2, (y + mY)/2);
         mX = x;
         mY = y;
     }
 }
 private void touch_up() {
     mPath.lineTo(mX, mY);
     mCanvas.drawPath(mPath, mPaint);// commit the path to our offscreen
     mPath.reset();// kill this so we don't double draw
 }
}

SignatureViewTutorialActivity.java

package com.platoevolved.signatureviewtutorial;
 
import android.app.Activity;
import android.os.Bundle;
 
public class SignatureViewTutorialActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}

One thought on “Creating a Simple Custom View in Android

Leave a Reply

Your email address will not be published. Required fields are marked *