In this series, we will create a finger-painting app for Android using touch interaction. The user will be able to select from a color palette, choose a brush size, erase, create a new drawing, or save their existing drawing to the device gallery.
Series Format
This series on Creating a Drawing App will be in three parts:
In the first part of the series we created the user interface. In the second part we implemented drawing on the canvas and choosing colors. In this the final part of the series we will introduce the ability to erase, to create new drawings, and to save a drawing to the gallery on the user device. We will look at the options you can use to enhance this app in future tutorials, including pattern fills and opacity.
Final Preview
1. Choosing Brush Sizes
Step 1
Last time we implemented drawing on the canvas, now we can let the user choose a brush size. The brush size options will appear when the user presses the brush button we added to the interface. To respond to this, extend the opening line of your main Activity class declaration to implement the OnClickListener interface:
public class MainActivity extends Activity implements OnClickListener
You will need the following import statements added to the class for this tutorial:
import java.util.UUID; import android.provider.MediaStore; import android.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; import android.view.View.OnClickListener; import android.widget.Toast;
Add the following instance variables to the class to store the three dimension values we defined last time:
private float smallBrush, mediumBrush, largeBrush;
Instantiate them in onCreate:
smallBrush = getResources().getInteger(R.integer.small_size); mediumBrush = getResources().getInteger(R.integer.medium_size); largeBrush = getResources().getInteger(R.integer.large_size);
We will use these later. You should already have an ImageButton instance variable in the main class named "currPaint" - extend that line to add another now for the drawing button:
private ImageButton currPaint, drawBtn;
In onCreate retrieve a reference to the button from the layout:
drawBtn = (ImageButton)findViewById(R.id.draw_btn);
Set the class up as a click listener for the button:
drawBtn.setOnClickListener(this);
Step 2
Add an onClick method to the class:
@Override public void onClick(View view){ //respond to clicks }
Inside the method, check for clicks on the drawing button:
if(view.getId()==R.id.draw_btn){ //draw button clicked }
We will be adding conditional blocks to the onClick method for the other buttons later.
Step 3
When the user clicks the button, we will display a dialog presenting them with the three button sizes. Inside the if block, create a Dialog and set the title:
final Dialog brushDialog = new Dialog(this); brushDialog.setTitle("Brush size:");
Let's define the Dialog layout in XML - add a new file in your app's "res/layout" folder, naming it "brush_chooser.xml" and entering the following outline:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical" > </LinearLayout>
Inside the Linear Layout, add a button for each size:
<ImageButton android:id="@+id/small_brush" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:contentDescription="@string/sml" android:src="@drawable/small" /> <ImageButton android:id="@+id/medium_brush" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:contentDescription="@string/med" android:src="@drawable/medium" /> <ImageButton android:id="@+id/large_brush" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:contentDescription="@string/lrg" android:src="@drawable/large" />
Each button has an ID for identification in the Activity Java code. You will notice that each one also has a content description attribute - add the specified strings to your "res/values" strings XML file:
<string name="sml">Small</string> <string name="med">Medium</string> <string name="lrg">Large</string>
As you can see in the layout file, each button also has a drawable file listed as its source attribute. Create new files for each of these in your "res/drawables" folder(s) now, starting with "small.xml" and entering the following content:
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:dither="true" android:shape="oval" > <size android:height="@dimen/small_brush" android:width="@dimen/small_brush" /> <solid android:color="#FF666666" /> </shape>
Notice that we use the dimension values we defined. Next add "medium.xml" to the drawables folder, entering the following shape:
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:dither="true" android:shape="oval" > <size android:height="@dimen/medium_brush" android:width="@dimen/medium_brush" /> <solid android:color="#FF666666" /> </shape>
Finally add "large.xml" with the following content:
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:dither="true" android:shape="oval" > <size android:height="@dimen/large_brush" android:width="@dimen/large_brush" /> <solid android:color="#FF666666" /> </shape>
Back in your main Activity class onClick method, after creating the Dialog and setting its title, you can now set the layout:
brushDialog.setContentView(R.layout.brush_chooser);
Step 4
Before we continue with the Dialog, let's alter the custom drawing View class to use different brush sizes. In your DrawingView class, add the following import statements for this tutorial:
import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.util.TypedValue;
Add two instance variables to the class:
private float brushSize, lastBrushSize;
We will use the first variable for the brush size and the second to keep track of the last brush size used when the user switches to the eraser, so that we can revert back to the correct size when they decide to switch back to drawing. In the setupDrawing method, before the code already in there, add the following to instantiate these variables:
brushSize = getResources().getInteger(R.integer.medium_size); lastBrushSize = brushSize;
We use the dimension value for the medium sized brush to begin with. You can now update the line in the method where you set the stroke width with a hard-coded value to use this variable value instead:
drawPaint.setStrokeWidth(brushSize);
Add the following method to the class to set the brush size:
public void setBrushSize(float newSize){ //update size }
Inside the method, update the brush size with the passed value:
float pixelAmount = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, newSize, getResources().getDisplayMetrics()); brushSize=pixelAmount; drawPaint.setStrokeWidth(brushSize);
We will be passing the value from the dimensions file when we call this method, so we have to calculate its dimension value. We update the variable and the Paint object to use the new size. Now add methods to get and set the other size variable we created:
public void setLastBrushSize(float lastSize){ lastBrushSize=lastSize; } public float getLastBrushSize(){ return lastBrushSize; }
We will call these methods from the main Activity class.
Step 5
Back in your main Activity class, let's complete the Dialog code in the onClick method. After setting the content view on the Dialog object, listen for clicks on the three size buttons, starting with the small one:
ImageButton smallBtn = (ImageButton)brushDialog.findViewById(R.id.small_brush); smallBtn.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v) { drawView.setBrushSize(smallBrush); drawView.setLastBrushSize(smallBrush); brushDialog.dismiss(); } });
We set the size using the methods we added to the custom View class as soon as the user clicks a brush size button, then immediately dismiss the Dialog. Next do the same for the medium and large buttons:
ImageButton mediumBtn = (ImageButton)brushDialog.findViewById(R.id.medium_brush); mediumBtn.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v) { drawView.setBrushSize(mediumBrush); drawView.setLastBrushSize(mediumBrush); brushDialog.dismiss(); } }); ImageButton largeBtn = (ImageButton)brushDialog.findViewById(R.id.large_brush); largeBtn.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v) { drawView.setBrushSize(largeBrush); drawView.setLastBrushSize(largeBrush); brushDialog.dismiss(); } });
Complete the draw button section of onClick by displaying the Dialog:
brushDialog.show();
The Dialog will display until the user makes a selection or goes back to the Activity.
Step 6
Use the new method to set the initial brush size in onCreate:
drawView.setBrushSize(mediumBrush);
2. Erasing
Step 1
Now let's add erasing to the app. In the custom drawing View class, add a boolean instance variable to act as a flag for whether the user is currently erasing or not:
private boolean erase=false;
Initially we will assume that the user is drawing, not erasing. Add the following method to the class:
public void setErase(boolean isErase){ //set erase true or false }
Inside the method, first update the flag variable:
erase=isErase;
Now alter the Paint object to erase or switch back to drawing:
if(erase) drawPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); else drawPaint.setXfermode(null);
If you're looking for an advanced topic to explore, have a look at the PorterDuff.Mode options.
Step 2
Back in the main Activity class, add another ImageButton to the list of instance variables:
private ImageButton currPaint, drawBtn, eraseBtn;
In onCreate, retrieve a reference to the button and set the class up to listen for clicks:
eraseBtn = (ImageButton)findViewById(R.id.erase_btn); eraseBtn.setOnClickListener(this);
Add a conditional statement for the button in onClick after the conditional for the draw button:
else if(view.getId()==R.id.erase_btn){ //switch to erase - choose size }
As with the draw button, we will let the user choose an eraser size from a Dialog. Inside the conditional block for the erase button, create and prepare the Dialog as before:
final Dialog brushDialog = new Dialog(this); brushDialog.setTitle("Eraser size:"); brushDialog.setContentView(R.layout.brush_chooser);
We use the same layout as the draw button Dialog. Setup click listeners for the size buttons as before, this time calling the new erase method we added to the View class:
ImageButton smallBtn = (ImageButton)brushDialog.findViewById(R.id.small_brush); smallBtn.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v) { drawView.setErase(true); drawView.setBrushSize(smallBrush); brushDialog.dismiss(); } }); ImageButton mediumBtn = (ImageButton)brushDialog.findViewById(R.id.medium_brush); mediumBtn.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v) { drawView.setErase(true); drawView.setBrushSize(mediumBrush); brushDialog.dismiss(); } }); ImageButton largeBtn = (ImageButton)brushDialog.findViewById(R.id.large_brush); largeBtn.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v) { drawView.setErase(true); drawView.setBrushSize(largeBrush); brushDialog.dismiss(); } });
We call the method to set the brush size as with the draw button, this time first setting the erase flag to true. Finally, display the Dialog:
brushDialog.show();
The user will be able to erase using touch interaction as with drawing:
Step 3
When the user clicks the draw button and chooses a brush size, we need to set back to drawing in case they have previously been erasing. In the three click listeners you added for the small, medium and large buttons in the draw button section of onClick, call the erase method with a false parameter - add this in each onClick before calling dismiss on the "brushDialog" object:
drawView.setErase(false);
When the user has been erasing and clicks a paint color button, we will assume that they want to switch back to drawing. In the paintClicked method, before the existing code, call the erase method, passing false:
drawView.setErase(false);
Still inside paintClicked, set the brush size back to the last one used when drawing rather than erasing:
drawView.setBrushSize(drawView.getLastBrushSize());
This type of processing is motivated by assumptions about what the user wants to do based on their actions - you could potentially enhance this app along these lines, so bear that in mind if you want to carry on working on the app later.
3. New Drawings
Step 1
We added a button for the user to start a new drawing, so let's implement that now. Add another to the list of ImageButton instance variables in your main Activity class:
private ImageButton currPaint, drawBtn, eraseBtn, newBtn;
Instantiate it with a reference to the button listed in the layout, in onCreate, also listening for clicks:
newBtn = (ImageButton)findViewById(R.id.new_btn); newBtn.setOnClickListener(this);
Step 2
In onClick, add another conditional block for the new button:
else if(view.getId()==R.id.new_btn){ //new button }
In your custom drawing View class, add a method to start a new drawing:
public void startNew(){ drawCanvas.drawColor(0, PorterDuff.Mode.CLEAR); invalidate(); }
The method simply clears the canvas and updates the display.
Back in the main Activity class conditional block for the new button in onCreate, let's verify that the user definitely wants to start a new drawing:
AlertDialog.Builder newDialog = new AlertDialog.Builder(this); newDialog.setTitle("New drawing"); newDialog.setMessage("Start new drawing (you will lose the current drawing)?"); newDialog.setPositiveButton("Yes", new DialogInterface.OnClickListener(){ public void onClick(DialogInterface dialog, int which){ drawView.startNew(); dialog.dismiss(); } }); newDialog.setNegativeButton("Cancel", new DialogInterface.OnClickListener(){ public void onClick(DialogInterface dialog, int which){ dialog.cancel(); } }); newDialog.show();
The Dialog lets the user change their mind, calling the new method if they decide to go ahead and start a new drawing, in which case the current drawing is cleared.
4. Save Drawings
Step 1
The one remaining part of the app functionality is the ability to save drawings to the device. Add the save button as the last in the sequence of ImageButton instance variables in the main Activity class:
private ImageButton currPaint, drawBtn, eraseBtn, newBtn, saveBtn;
Instantiate it and listen for clicks in onCreate:
saveBtn = (ImageButton)findViewById(R.id.save_btn); saveBtn.setOnClickListener(this);
Add a conditional for it in onClick:
else if(view.getId()==R.id.save_btn){ //save drawing }
Let's use a similar algorithm to the one we used for creating new drawings, to check that the user wants to go ahead and save:
AlertDialog.Builder saveDialog = new AlertDialog.Builder(this); saveDialog.setTitle("Save drawing"); saveDialog.setMessage("Save drawing to device Gallery?"); saveDialog.setPositiveButton("Yes", new DialogInterface.OnClickListener(){ public void onClick(DialogInterface dialog, int which){ //save drawing } }); saveDialog.setNegativeButton("Cancel", new DialogInterface.OnClickListener(){ public void onClick(DialogInterface dialog, int which){ dialog.cancel(); } }); saveDialog.show();
If the user chooses to go ahead and save, we need to output the currently displayed View as an image. In the onClick method for the save Dialog, start by enabling the drawing cache on the custom View:
drawView.setDrawingCacheEnabled(true);
Now attempt to write the image to a file:
String imgSaved = MediaStore.Images.Media.insertImage( getContentResolver(), drawView.getDrawingCache(), UUID.randomUUID().toString()+".png", "drawing");
Take a moment to look over this code. We use the insertImage method to attempt to write the image to the media store for images on the device, which should save it to the user gallery. We pass the content resolver, drawing cache for the displayed View, a randomly generated UUID string for the filename with PNG extension and a short description. The method returns the URL of the image created, or null if the operation was unsuccessful - this lets us give user feedback:
if(imgSaved!=null){ Toast savedToast = Toast.makeText(getApplicationContext(), "Drawing saved to Gallery!", Toast.LENGTH_SHORT); savedToast.show(); } else{ Toast unsavedToast = Toast.makeText(getApplicationContext(), "Oops! Image could not be saved.", Toast.LENGTH_SHORT); unsavedToast.show(); }
Destroy the drawing cache so that any future drawings saved won't use the existing cache:
drawView.destroyDrawingCache();
Finally, add the following permission to your project manifest file:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
On browsing to the device gallery, the user should now be able to see their drawing image:
Conclusion
This tutorial completes the functionality for our basic drawing app! When you run the app you should be able to draw, choose colors, choose brush and eraser sizes, start new drawings and save drawings to the gallery. We have worked through the basic process of facilitating drawing using touchscreen interaction on Android, but there are many ways in which you could enhance the application so try experimenting with it to build in your own additional functionality. In future tutorials we will cover using pattern fills rather than simply drawing with solid colors, using opacity, and letting the user choose an opacity level. We will also cover drawing on Android devices where the interaction model is not a touchscreen, for example with a mouse or trackball.
Comments