Android Drawing App Tutorial — Pt. 2

This blog post is part two of my Android Drawing App tutorials. It is strongly encouraged that you finish part one of this tutorial before proceeding with this one. At the end of part one of this tutorial, we were able to draw something with our Android drawing app, and we added some Icons to the bottom toolbar. We will now proceed to implement the functionalities that each of those icons represents. You can download the source code from here.

Implement Delete Drawing

The implementation for deleting or erasing everything in the screen is actually quite simple, all we need to do is to clear the screen like this:

/** Start new Drawing */
public void eraseAll() {
drawCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
invalidate();
}

However before you call the above method, you may want to give the user the option to confirm the wipe the screen call. We accomplish this with an Alert dialog, go ahead and add this method to your MainActivity.

private void deleteDialog(){
AlertDialog.Builder deleteDialog = new AlertDialog.Builder(this);
deleteDialog.setTitle(getString(R.string.delete_drawing));
deleteDialog.setMessage(getString(R.string.new_drawing_warning));
deleteDialog.setPositiveButton("Yes", new DialogInterface.OnClickListener(){
public void onClick(DialogInterface dialog, int which){
mCustomView.eraseAll();
dialog.dismiss();
}
});
deleteDialog.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
});
deleteDialog.show();
}

You need to add an instance of the CustomView at the top of the MainActivity like this private CustomView mCustomView, and in the onCreate() method go ahead and instantiate this class like this mCustomView = (CustomView)findViewById(R.id.custom_view);

And with that, you can now call the delete method whenever the user touches the icon you designate as the delete or erase icon. Update your handleDrawingIconTouched method like this:

private void handleDrawingIconTouched(int itemId) {
switch (itemId){
case R.id.action_delete:
deleteDialog();
break;
}
}

Implement Undo and Redo

Follow the steps below to implement redo and undo in your Android drawing app.

Step 1: Add Path List — near the top of your CustomView.java class, add two array list to hold the paths that are drawn on the screen and the paths that have been removed from the screen like this:

private ArrayList<Path> paths = new ArrayList<Path>();
private ArrayList<Path> undonePaths = new ArrayList<Path>();

Step 2: Update Touch Event Handler — now that we need to record the history of the paths we draw, we need to handle our drawing differently. Instead of adding our updating our drawing in the onTouchEvent() method we can create separate private methods and then call them from the onTouchEvent() method like this:

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

switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
touch_start(touchX, touchY);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
touch_move(touchX, touchY);
invalidate();
break;
case MotionEvent.ACTION_UP:
touch_up();
invalidate();
break;
default:
return false;
}
return true;
}

Step 3: Create Touch Start Method — the touch start methods is called when the finger touches the screen which is considered a MotionEvent.Action_Down event and when this happens, we want to clear the parts list and reset the Path object. Add the following method to your app:

private void touch_start(float x, float y) {
undonePaths.clear();
drawPath.reset();
drawPath.moveTo(x, y);
mX = x;
mY = y;
}

Step 3: Create Touch Move Method — when the user moved from point A to the point, we will need to evaluate the tolerance value that we set in the last step and then apply this method adapted from the Android FingerPaint App.

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) {
drawPath.quadTo(mX, mY, (x + mX)/2, (y + mY)/2);
mX = x;
mY = y;
}
}

Step 4: Create Touch Up Method — when the user lifts up their finger from the screen, we want to paint what they have just drawn on the screen as well as save it to our path like like this:

private void touch_up() {
drawPath.lineTo(mX, mY);
drawCanvas.drawPath(drawPath, drawPaint);
paths.add(drawPath);
drawPath = new Path();

}

Step 5: Update the onDrawMethod — we will need to update our onDraw() so that it draws from the path list.

protected void onDraw(Canvas canvas) {
for (Path p : paths) {
canvas.drawPath(p, drawPaint);
}
canvas.drawPath(drawPath, drawPaint);
}

Step 6: Add Undo and Redo Methods — now we need to add the methods that will trigger the undo and the redo. Add following two methods to your custom view.

public void onClickUndo () {
if (paths.size()>0)
{
undonePaths.add(paths.remove(paths.size()-1));
invalidate();
}

}

public void onClickRedo (){
if (undonePaths.size()>0)
{
paths.add(undonePaths.remove(undonePaths.size()-1));
invalidate();
}

}

Step 7: Call the Undo and Redo Methods — update your handleDrawingIconTouched() method in MainActivity to handle undo and redo button click.

private void handleDrawingIconTouched(int itemId) {
switch (itemId){
case R.id.action_delete:
deleteDialog();
break;
case R.id.action_undo:
mCustomView.onClickUndo();
break;
case R.id.action_redo:
mCustomView.onClickRedo();
break;
}
}

Implement Share Drawing

In this section of the tutorial, we will implement the ability to share our drawing. To share our picture, we will necessarily take a screenshot of what we drew, save that as an image and then share it with share intent. Follow the steps below to share your drawing.

Step 1: Request Permission — we will save the drawing we wan to save as an image to the device external folder, so we need to write permission for the external folder. Add this line of code to your AndroidManifest.xml file:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Step 2: Add Share Method — add the method that needs to be called to perform a share, we will leave this method blank for now and populate it shortly. Add the method private void shareDrawing() to your MainActivity.java file.

private void handleDrawingIconTouched(int itemId) {
switch (itemId){
case R.id.action_delete:
deleteDialog();
break;
case R.id.action_undo:
mCustomView.onClickUndo();
break;
case R.id.action_redo:
mCustomView.onClickRedo();
break;
case R.id.action_share:
shareImage();
break;
}
}

Step 3: Call Share Method — update the handleDrawingIconTouched() method to call the share method whenever the icon representing share is touched.

private void handleDrawingIconTouched(int itemId) {
switch (itemId){
case R.id.action_delete:
deleteDialog();
break;
case R.id.action_undo:
mCustomView.onClickUndo();
break;
case R.id.action_redo:
mCustomView.onClickRedo();
break;
case R.id.action_share:
shareImage();
break;
}
}

Step 4: Populate Share Method — we can now populate the share method with the following code

private void shareDrawing() {
mCustomView.setDrawingCacheEnabled(true);
mCustomView.invalidate();
String path = Environment.getExternalStorageDirectory().toString();
OutputStream fOut = null;
File file = new File(path,
"android_drawing_app.png");
file.getParentFile().mkdirs();

try {
file.createNewFile();
} catch (Exception e) {
Log.e(LOG_CAT, e.getCause() + e.getMessage());
}

try {
fOut = new FileOutputStream(file);
} catch (Exception e) {
Log.e(LOG_CAT, e.getCause() + e.getMessage());
}

if (mCustomView.getDrawingCache() == null) {
Log.e(LOG_CAT,"Unable to get drawing cache ");
}

mCustomView.getDrawingCache()
.compress(Bitmap.CompressFormat.JPEG, 85, fOut);

try {
fOut.flush();
fOut.close();
} catch (IOException e) {
Log.e(LOG_CAT, e.getCause() + e.getMessage());
}

Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_STREAM,Uri.fromFile(file));
shareIntent.setType("image/png");
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(shareIntent, "Share image"));


}

Code Walk through

Here is what is going on in the above code:

  1. First, we enabled drawing cache on our custom view
  2. We created a file with the android_drawing_app.png
  3. We then get the cache of our drawing and compress it into a JPEG
  4. Then we create a share intent to deliver our drawing.

Go ahead and run your app and make sure that you can share an image, if not use the comment box to let me know what you need help with. If you have not already done so, You can download the source code from here.

Implement Change Brush Size

brush_size_picker_dialog_framed1

When we initiated our custom view, we set the brush size to 20. In this section, we want to implement the code that will give the user the option to choose a different size. I have chosen to use a Seekbar to implement this feature with values from 1 to 50. So the user will have a dialog like the one below to drag left of right to pick a brush size.

Follow the steps below to implement brush chooser to your Android drawing app.

Step 1: Add Set Brush Methods — in your CustomView.java class file add the following methods that will set and get the brush size

public void setBrushSize(float newSize) {
float pixelAmount = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
newSize, getResources().getDisplayMetrics());
currentBrushSize = pixelAmount;
canvasPaint.setStrokeWidth(newSize);
}

public void setLastBrushSize(float lastSize){
lastBrushSize=lastSize;
}

public float getLastBrushSize(){
return lastBrushSize;
}

Step 2: Add Packages — the alert dialog that will show the choose new brush dialog will be defined in a separate file and when a new brush size is selected we will use a listener to tell the MainActivity that a new size has been selected. This listener will also be defined in a separate file. So for code organization let use go ahead and two packages: “dialogs” and “listeners”.

Step 3: Add Listener — in the listeners package add an Interface OnNewBrushSizeSelectedListener.java and here is the content of this Interface (take not that an Interface is not a class).

public interface OnNewBrushSizeSelectedListener {
void onNewBrushSizeSelected(float newBrushSize);
}

Step 4: Add Dialog — In the dialogs, package add a blank Fragment with the name BrushSizeChooserFragment.java and here is the content of the layout of this Fragment

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:gravity="center_horizontal"
android:padding="@dimen/margin_padding_normal"
android:layout_height="match_parent">

<TextView
android:id="@+id/text_view_brush_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"/>

<TextView
android:id="@+id/text_view_min_value"
android:layout_below="@+id/text_view_brush_size"
android:layout_alignParentLeft="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

<SeekBar
android:id="@+id/seek_bar_brush_size"
android:layout_below="@+id/text_view_brush_size"
android:layout_toRightOf="@+id/text_view_min_value"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:max="@integer/max_size"
android:progress="2"/>

<TextView
android:id="@+id/text_view_max_value"
android:layout_below="@+id/text_view_brush_size"
android:layout_toRightOf="@+id/seek_bar_brush_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

</RelativeLayout>

Step 5: Call the Dialog — before implementing the dialog fragment go ahead and add the code below to your MainActivity. This will be the method that calls the dialog, ignore the red squiggly lines for now.

private void brushSizePicker(){
//Implement get/set brush size
BrushSizeChooserFragment brushDialog = BrushSizeChooserFragment.NewInstance((int) mCustomView.getLastBrushSize());
brushDialog.setOnNewBrushSizeSelectedListener(new OnNewBrushSizeSelectedListener() {
@Override
public void OnNewBrushSizeSelected(float newBrushSize) {
mCustomView.setBrushSize(newBrushSize);
mCustomView.setLastBrushSize(newBrushSize);
}
});
brushDialog.show(getSupportFragmentManager(), "Dialog");
}

And now you can update your handleDrawingIconTouched() method to call the method above when the icon representing brush size is selected. Here is the update method.

private void handleDrawingIconTouched(int itemId) {
switch (itemId){
case R.id.action_delete:
deleteDialog();
break;
case R.id.action_undo:
mCustomView.onClickUndo();
break;
case R.id.action_redo:
mCustomView.onClickRedo();
break;
case R.id.action_share:
shareDrawing();
break;
case R.id.action_brush:
brushSizePicker();
break;
}
}

Step 6: Implement Dialog Fragment — copy and paste the code below to your BrushSizeChooserFragment.java , you will need to add required text to your res/string folder.

public class BrushSizeChooserFragment extends DialogFragment {

private float selectedBrushSize;
private OnNewBrushSizeSelectedListener mListener;
private SeekBar brushSizeSeekBar;
private TextView minValue, maxValue, currentValue;
private int currentBrushSize ;

/**
*
* @param listener an implementation of the listener
*
*/
public void setOnNewBrushSizeSelectedListener(
OnNewBrushSizeSelectedListener listener){
mListener = listener;
}

public static BrushSizeChooserFragment NewInstance(int size){
BrushSizeChooserFragment fragment = new BrushSizeChooserFragment();
Bundle args = new Bundle();
if (size > 0){
args.putInt("current_brush_size", size);
fragment.setArguments(args);
}
return fragment;
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if (args != null && args.containsKey("current_brush_size")){
int brushSize = args.getInt("current_brush_size", 0);
if (brushSize > 0){
currentBrushSize = brushSize;
}
}
}

@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// Begin building a new dialog.
final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

// Get the layout inflater.
final LayoutInflater inflater = getActivity().getLayoutInflater();

// Inflate the layout for this dialog.
final View dialogView = inflater.inflate(R.layout.dialog_brush_size_chooser, null);
if (dialogView != null) {
//set the starting value of the seek bar for visual aide
minValue = (TextView)dialogView.findViewById(R.id.text_view_min_value);
int minSize = getResources().getInteger(R.integer.min_size);
minValue.setText(minSize + "");

maxValue = (TextView)dialogView.findViewById(R.id.text_view_max_value);
maxValue.setText(String.valueOf(getResources().getInteger(R.integer.max_size)));


currentValue = (TextView)dialogView.findViewById(R.id.text_view_brush_size);
if (currentBrushSize > 0){
currentValue.setText(getResources().getString(R.string.label_brush_size) + currentBrushSize);
}

brushSizeSeekBar = (SeekBar)dialogView.findViewById(R.id.seek_bar_brush_size);
brushSizeSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
int progressChanged = 0;

@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
progressChanged = progress;
currentValue.setText(getResources().getString(R.string.label_brush_size) + progress);

}

@Override
public void onStartTrackingTouch(SeekBar seekBar) {

}

@Override
public void onStopTrackingTouch(SeekBar seekBar) {
mListener.OnNewBrushSizeSelected(progressChanged);
}
});

}

builder.setTitle("Choose new Brush Size")
.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.setView(dialogView);


return builder.create();

}

In the code above we used newInstance method to pass in the currently selected brush size to the Fragment. This way we can set the Seekbar to the current size thereby giving the user a visual feedback on what size they currently have before they make a new selection.

Run the app now, and you should be able to change the size of the brush.

Conclusion

This concludes my two-part tutorial series on creating Android drawing app. You should now have a functioning basic drawing app. If you want to expand this app further then you may want to take my course on the topic.


Originally published at Val Okafor.