Skip to content

Instantly share code, notes, and snippets.

@ajrussellaudio
Created September 5, 2016 14:02
Show Gist options
  • Save ajrussellaudio/2582e2e81425407d22f778634a181ec8 to your computer and use it in GitHub Desktop.
Save ajrussellaudio/2582e2e81425407d22f778634a181ec8 to your computer and use it in GitHub Desktop.

#Canvas

##Learning Objectives

  • Learn about canvas on Android
  • Create a simple drawing app

#Canvas [i]: This lesson is taken from this Android Canvas Tutorial.

##Set up project First of all we need to set up a project for our app, so let's do that now.

// setup 

launchpad > android studio

Start a new android project

// application settings 

Application name: CanvasExample
Company Domain: com.codeclan.example

Select the sdk version version 16 (Jelly Bean)

Select 'Add No Activity'

##Creating our Layout

So we will start by creating our layout xml file for our app. We will just call this activity_main.xml

[i]: insert instructions on creating layout file, as a reminder

This time we, rather than the root layout being a LinearLayout, we are going to use a FrameLayout. This is because we are going to use a Clear Canvas button which will clear our canvas. Using a FrameLayout helps us to place the button above the canvas, like putting it in a separate layout on top.

<!-- activity_main.xml -->

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FFFFFF"
    android:orientation="vertical">

</FrameLayout>

###Adding our button to our layout

We can now add our button to our layout:

<!-- activity_main.xml -->

  <Button
        android:id="@+id/clear_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|center"
        android:text="@string/clear_canvas" />

As we are using a string resource we now add a new entry to our strings.xml file:

<!-- res/values/strings.xml -->

  <string name="clear_canvas">Clear Canvas</string>

##Create Custom Canvas View

So let's create our custom CanvasView:

<!-- activity_main.xml -->

  <com.codeclan.example.canvasexample.CanvasView
        android:id="@+id/signature_canvas"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:textColor="#FFFFFF" />

[i]: before going on to the Java code, remember to add the layout to the AndroidManifest.xml file - just so you don't forget later :-)

Add the following to the AndroidManifest.xml file, just before the ````` tag:

<activity android:name=".CanvasExample"
    android:label="@string/app_name">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>

</activity>

So the manifest file should now look like:

<!-- android_manifest.xml -->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.sandy.canvasexample">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity android:name=".CanvasExample"
            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>

###Creating the source code for our activity

Create a Java class called CanvasExample:

// CanvasExample.java

public class CanvasExample extends AppCompatActivity {

    private CanvasView mCustomCanvas;

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

        mCustomCanvas = (CanvasView) findViewById(R.id.signature_canvas);
    }
}

Add the code for our clear button:

// CanvasExample.java
public class CanvasExample extends AppCompatActivity {

    ...
    private Button mClearCanvasButton;
    ...
}

We now need to assign a value to our button, using findViewById:

// CanvasExample.java
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        mCustomCanvas = (CanvasView)findViewById(R.id.signature_canvas);
        mClearCanvasButton = (Button)findViewById(R.id.clear_button);     <== ADDED
        ...
      }

Finally we need to add the listener for our button:

// CanvasExample.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    mCustomCanvas = (CanvasView)findViewById(R.id.signature_canvas);
    mClearCanvasButton = (Button)findViewById(R.id.clear_button); 
                                                                       __
    mClearCanvasButton.setOnClickListener(new View.OnClickListener() {   \
                @Override                                                 |
                public void onClick(View view) {                          | <==ADDED
                }                                                         |
            });                                                        __/
  }

To clear our canvas, we are going to call a method called clearCanvas on our CustomCanvas object i.e.

  mCustomCanvas.clearCanvas()
// CanvasExample.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    mCustomCanvas = (CanvasView)findViewById(R.id.signature_canvas);
    mClearCanvasButton = (Button)findViewById(R.id.clear_button); 
                                                                       
    mClearCanvasButton.setOnClickListener(new View.OnClickListener() {   
                @Override                                                 
                public void onClick(View view) {                          
                    mCustomCanvas.clearCanvas();    <== ADDED
                }                                                         
            });
  }

We'll implement the clearCanvas method in the Java class for our CustomCanvas.

##Creating the code for our Canvas View

Create a java class called CanvasView. Rather than inheriting from AppCompatActivity this class will inherit from the View class, as we are creating a custom view:

// CanvasView.java

import android.view.View;

public class CanvasView extends View {

}

We now need to add a constructor. This constructor takes two arguments, our old friend the Context, and an AttributeSet object, which refers to the attributes on a View in an xml layout. The first thing we do in our constructor is to call the constructor for the superclass (i.e. View), passing in the Context and AttributeSet arguments passed in i.e.:

// CanvasView.java

import android.view.View;

public class CanvasView extends View {

  public CanvasView(Context c, AttributeSet attrs) {
    super(c, attrs);
  }
}

We are going to be using the context throughout the class, so lets declare an instance variable of type Context and initialise it in our constructor, using the context passed in i.e.:

// CanvasView.java

import android.view.View;

public class CanvasView extends View {

  Context mContext; <== ADDED

  public CanvasView(Context c, AttributeSet attrs) {
    super(c, attrs);
    mContext = c;   <== ADDED
  }
}

The next thing we need to do is create an instance of the Path class. This class basically deals with drawing lines in the canvas. We'll declare an instance variable for this Path object, as it will be used throughout the class and initialise it by calling the constructor for the Path class:

// CanvasView.java

import android.view.View;

public class CanvasView extends View {
  private Path mPath;     <== ADDED

  public CanvasView(Context c, AttributeSet attrs) {
    super(c, attrs);

    mPath = new Path();   <== ADDED
  }
}

We now need to do the same for the Paint class. It is the Paint class which holds the style and colour information about how to draw geometries, text and bitmaps.

// CanvasView.java

import android.view.View;

public class CanvasView extends View {

  private Path mPath;
  Context mCcontext;
  private Paint mPaint;   <== ADDED
  
  public CanvasView(Context c, AttributeSet attrs) {
    super(c, attrs);
    context = c;

    mPath = new Path();

    mPaint = new Paint(); <== ADDED
  }
}

###Adding Attributes to a Paint Object

We can set our own attributes on mPaint, depending on how we want things to look:

  mPaint.setAntiAlias(true);

When this is set to true it results in the edges being smoothed out on what is being drawn.

  mPaint.setColor(Color.BLACK);

Self-explanatory really :-)

  mPaint.setStyle(Paint.Style.STROKE);

Geometry and text drawn with this style will be stroked, respecting the stroke-related fields on the paint.

  mPaint.setStrokeJoin(Paint.Join.ROUND);

This means that outer edges of a join meet in a circular arc.

  mPaint.setStrokeWidth(4f);

This sets the stroke width (i.e. in our app, the width of a line). Note that it takes an argument of type float .

Thus our completed constructor should look something like:

// CanvasView.java

import android.view.View;

public class CanvasView extends View {

  private Path mPath;
  Context mCcontext;
  private Paint mPaint;
  
  public CanvasView(Context c, AttributeSet attrs) {
    super(c, attrs);
    context = c;

    // we set a new Path
    mPath = new Path();

    // and we set a new Paint with the desired attributes
    mPaint = new Paint();
    mPaint.setAntiAlias(true);
    mPaint.setColor(Color.BLACK);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeJoin(Paint.Join.ROUND);
    mPaint.setStrokeWidth(4f);
  }
}
// CanvasView.java

import android.view.View;

public class CanvasView extends View {

  public int mWidth;
  public int mHeight;
  private Bitmap mBitmap;
  private Canvas mCanvas;
  private Path mPath;
  Context mContext;
  private Paint mPaint;
  private float mX, mY;
  private static final float TOLERANCE = 5;

  public CanvasView(Context c, AttributeSet attrs) {
    super(c, attrs);
    mContext = c;

    // we set a new Path
    mPath = new Path();

    // and we set a new Paint with the desired attributes
    mPaint = new Paint();
    mPaint.setAntiAlias(true);
    mPaint.setColor(Color.BLACK);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeJoin(Paint.Join.ROUND);
    mPaint.setStrokeWidth(4f);
  }
}

##Handling Touch Screen Motion Events

Let's add a method to get the X and Y co-ordinates so that we can make our path moves. We do this by overriding the onTouchEvent method, which is used to handle touch screen motion events. This method retuns 'true' if the event is handled, otherwise it returns 'false'. We are going to handle the event, so we will return 'true':

// CanvasView.java

@Override
public boolean onTouchEvent(MotionEvent event) {
  
  return true;
}

The first thing we need to do is get the X and Y co-ordinates. We can do this by calling the getX and getY methods on the MotionEvent object:

// CanvasView.java

@Override
public boolean onTouchEvent(MotionEvent event) {
  float x = event.getX();
  float y = event.getY();

  return true;
}

We now need to handle when the user touches down on the screen, moves along the screen, and then releases their touch from the screen. This corresponds to three MotionEvent constants:

  • MotionEvent.ACTION_DOWN
  • MotionEvent.ACTION_MOVE
  • MotionEvent.ACTION_UP

We'll use a switch statement to handle the three events. At the moment, we'll do a call to invalidate() for each, which means that this will force a redraw of the canvas:

// CanvasView.java

@Override
public boolean onTouchEvent(MotionEvent event) {
  float x = event.getX();
  float y = event.getY();

  switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
      invalidate();
      break;
    case MotionEvent.ACTION_MOVE:
      invalidate();
      break;
    case MotionEvent.ACTION_UP:
      invalidate();
      break;
  }
  return true;
}
// CanvasView.java

@Override
public boolean onTouchEvent(MotionEvent event) {
  float x = event.getX();
  float y = event.getY();

  switch (event.getAction()) {
  case MotionEvent.ACTION_DOWN:
    startTouch(x, y);
    invalidate();
    break;
  case MotionEvent.ACTION_MOVE:
    moveTouch(x, y);
    invalidate();
    break;
  case MotionEvent.ACTION_UP:
    upTouch();
    invalidate();
    break;
  }
  return true;
}

###startTouch

// CanvasView.java
// when ACTION_DOWN start touch according to the x,y values
  private void startTouch(float x, float y) {
    mPath.moveTo(x, y);
    mX = x;
    mY = y;
  }

mPath.moveTo - Set the beginning of the next contour to the point (x,y).

###moveTouch

// CanvasView.java
  // when ACTION_MOVE move touch according to the x,y values
  private void moveTouch(float x, float y) {
    float dx = Math.abs(x - mX);
    float dy = Math.abs(y - mY);
    if (dx >= TOLERANCE || dy >= TOLERANCE) {
      mPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
      mX = x;
      mY = y;
    }
  }

mPath.quadTo - Add a curve from the last point, approaching control point (x1,y1), and ending at (x2,y2).

###upTouch

// CanvasView.java

  // when ACTION_UP stop touch
  private void upTouch() {
    mPath.lineTo(mX, mY);
  }

mPath.lineTo - Add a line from the last point to the specified point (x,y).

// CanvasView.java

public class CanvasView extends View {
  ...
  private Bitmap mBitmap;  <== ADDED
  private Canvas mCanvas;  <== ADDED

  @Override
  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    mCanvas = new Canvas(mBitmap);
  }
}

##Clearing the canvas

We will now implement our clearCanvas method. This function contains two lines of code. The first is a call to the reset() method of the Path class. This basically clears all lines from the path i.e. making it empty.

  mPath.reset();

The second line is a call to invalidate() i.e. we want to re-draw the canvas after mPath has been cleared:

  invalidate();

Thus our method should look something like:

// CanvasView.java
public void clearCanvas() {
  mPath.reset();
  invalidate();
}

##Drawing our path on the canvas To draw our path onto our canvas, we need to override the onDraw method:

//CanvasView.java

@Override
protected void onDraw(Canvas canvas) {
  super.onDraw(canvas);
  canvas.drawPath(mPath, mPaint);
}

##TASK

Look into extending this app, to add one or more of the following features:

  • change the ink color
  • change the width of the stroke width
  • enable to user to 'rub out'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment