#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'