User Tools

Site Tools


android-labs-s14:class-06

Class 06

In this class, we'll be learning about:

  • Canvas, Bitmap, Paint, Path
  • Context Menu
  • Alert Dialog

Drawing Board app

Step 01: Create a place for the Canvas in FragmentTwo

If user clicks on 'Create New' Button on the Main Screen, then fragment_02 should look something like this:

As you can see, we have a textbox to put in a drawing title, with a button 'Save' next to it. We can club this EditText and Button in a RelativeLayout. Also, we need to provide some space to put the canvas (we'll name it DrawingView) below, which we'll be creating programatically. To do this we can have following layout in fragment_02.xml:

fragment_02.xml
<?xml version="1.0" encoding="utf-8"?>
 
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
 
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
 
        <!-- Welcome Title -->
        <TextView
            android:id="@+id/textview_welcome"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="@string/two_welcome"
            android:textSize="24sp"
            android:textStyle="bold" />
 
        <!-- Outer Layout for Canvas -->
        <LinearLayout
            android:id="@+id/layout_drawing"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="5dip"
            android:orientation="vertical"
            android:visibility="gone" >
 
            <RelativeLayout
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="@dimen/activity_horizontal_margin"
                android:layout_marginRight="@dimen/activity_horizontal_margin" >
 
                <EditText
                    android:id="@+id/edittext_drawing"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentLeft="true"
                    android:layout_toLeftOf="@+id/btn_two_save"
                    android:hint="@string/two_drawing" />
 
                <Button
                    android:id="@id/btn_two_save"
                    android:layout_width="100dp"
                    android:layout_height="wrap_content"
                    android:layout_alignParentRight="true"
                    android:onClick="onButtonClick"
                    android:text="Save" />
            </RelativeLayout>
 
            <!-- We will PROGOMATICALLY insert Canvas here -->
 
        </LinearLayout>
    </FrameLayout>
 
</LinearLayout>

Step 02: Adding **DrawingView**

Before we implement the click callback of 'Create New' or 'Open Existing' buttons, let's create a class which defines the DrawingView, on which our app user can draw as on a painting board. For that, add a class named DrawingView.java (extending View) inside src/com.example.board package name with the following methods implemented.

DrawingView.java
/**
 * Class representing a View on which one can draw like a paint board.
 */
public class DrawingView extends View 
{
	private Bitmap mBitmap;	// To hold the pixels
	private Canvas mCanvas;	// To host 'draw' calls
	private Path mPath;	// A drawing primitive used in this case
	private Paint mPaint; 	// To define colors and styles of drawing
	private ArrayList<Float> mPartsDrawingList;		// To store individual lines
	public ArrayList<ArrayList<Float>> mOverallDrawingList;	// To store overall drawing 
	private float mX, mY;	// Current location
 
	public DrawingView(Context c) 
	{
		super(c);
 
		// Set Drawing Paint Attributes
		setPaint();
 
		mBitmap = Bitmap.createBitmap(400, 580, Bitmap.Config.ARGB_8888); // 'ARGB_8888' => Each pixel is stored on 4 bytes.
		mCanvas = new Canvas(mBitmap);
		mPath = new Path();
 
		mOverallDrawingList = new ArrayList<ArrayList<Float>>();		
	}
 
	/**
	 * Method to set Paint attributes
	 */
	private void setPaint() 
	{
		mPaint = new Paint();
 
		// Setting different properties of Paint object. Feel free to play with these.
		mPaint.setAntiAlias(true);
		mPaint.setDither(true);
		mPaint.setColor(0xFFFFFFFF); // White
		mPaint.setStyle(Paint.Style.STROKE);
		mPaint.setStrokeJoin(Paint.Join.ROUND);
		mPaint.setStrokeCap(Paint.Cap.ROUND);
		mPaint.setStrokeWidth(8);
	}
 
	/**
	 * Method to set overall drawing list to a given list.
	 * @param drawingList	Given drawing list
	 */
	public void setOverallDrawingList(ArrayList<ArrayList<Float>> drawingList)
	{
		mOverallDrawingList = drawingList;
	}
 
	@Override
	protected void onDraw(Canvas canvas) 
	{
		canvas.drawBitmap(mBitmap, 0 /*left*/, 0 /*top*/, null /*Paint*/);
		canvas.drawPath(mPath, mPaint);
	}	
}

Let's learn a little more about Canvas.

Canvas

A Canvas works for you as a pretense, or interface, to the actual surface upon which your graphics will be drawn — it holds all of your “draw” calls. Via the Canvas, your drawing is actually performed upon an underlying Bitmap, which is placed into the window. In the event that you're drawing within the onDraw() callback method, the Canvas is provided for you and you need only place your drawing calls upon it. If you need to create a new Canvas, then you must define the Bitmap upon which drawing will actually be performed. The Bitmap is always required for a Canvas.

So, to draw something, you need 4 basic components:

  1. a Bitmap to hold the pixels
  2. a Canvas to host the draw calls (writing into the bitmap)
  3. a drawing primitive (e.g. Rect, Path, Text)
  4. a Paint to describe the colors and styles for the drawing.

Cool! So, now is the time to add a touch element to the DrawingView.

To handle touch screen motion events for any View, we can override and implement onTouchEvent() method. We can detect different kind of motions using the value of MotionEvent object, which is passed as the argument value in the onTouchEvent() method whenever any touch gesture takes place over that View. To handle touch gestures on DrawingView, we can implement following methods in this class:

DrawingView.java
	@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;
	}
 
	/**
	 * Callback for the case when pressed gesture has started. 
	 * 
	 * @param x		Inital Starting loc X
	 * @param y		Inital Starting loc Y
	 */
	private void touch_start(float x, float y)
	{		
		mPath.reset();
		mPath.moveTo(x, y);
		mX = x;
		mY = y;
 
		mPartsDrawingList = new ArrayList<Float>();
		mPartsDrawingList.add(mX);
		mPartsDrawingList.add(mY);
	}
 
	private static final float TOUCH_TOLERANCE = 0;
 
	/**
	 * Callback for the case when a change happens during a press gesture
	 * 
	 * @param x		Most recent point X
	 * @param y		Most recent point 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;
		}
 
		mPartsDrawingList.add(mX);
		mPartsDrawingList.add(mY);
	}
 
	/**
	 * Callback for the case when pressed gesture is finished.
	 */
	private void touch_up() 
	{
		mPath.lineTo(mX, mY);
 
		// Commit the path to our offscreen
		mCanvas.drawPath(mPath, mPaint);
 
		// Reset path so we don't double draw
		mPath.reset();
 
		mOverallDrawingList.add(mPartsDrawingList);
	}

There may be a case when Visibility or the Size of the DrawingView changes, may be because of the change in device orientation or change in the visibility of an ancestor of the DrawingView. To handle such cases, let's implement following fallback methods in the class as well:

DrawingView.java
	@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);
		redrawPath(mCanvas);
	}
 
	@Override
	protected void onVisibilityChanged(View changedView, int visibility)
	{
		super.onVisibilityChanged(changedView, visibility);
 
		if (visibility == View.VISIBLE)
		{
			if (mBitmap != null && mCanvas != null)
			{
				redrawPath(mCanvas);
				invalidate();
			}
		}
	}
 
	/*
	 * Helper method to redraw the entire path again on Canvas.
	 */
	private void redrawPath(Canvas canvas)
	{
		int numLines = mOverallDrawingList.size();
		for (int i = 0; i < numLines; i++)
		{
			// Un-flatten the drawing points
			ArrayList<Float> partDrawingList = mOverallDrawingList.get(i);
			touch_start(partDrawingList.get(0), partDrawingList.get(1));
 
			int partDrawingListSize = partDrawingList.size();
 
			for (int j = 2; j < partDrawingListSize; j += 2)
			{
				// Simulate the move gestures
				touch_move(partDrawingList.get(j), partDrawingList.get(j + 1));
			}
 
			mPath.lineTo(mX, mY);
 
			// Commit the path to our offscreen
			canvas.drawPath(mPath, mPaint);
 
			// Reset path so we don't double draw
			mPath.reset();
		}
	}

Step 03: [TABLETS] Create New Drawing Board

In this step, we want to bring up a clean drawing board whenever user clicks on 'Create New' button.

  • So, firstly, download a background image from here.
  • Create a folder named drawable inside res folder, and copy the downloaded file to that folder.
  • To add DrawingView to FragmentTwo, we'll implement a method createNewBoard() in FragmentTwo class, which will be called when user clicks on 'Create New' button.
FragmentTwo.java
	private DrawingView mDrawingView = null;
 
	/**
	 * Method to add a new DrawingView to the fragment. 
	 */
	public void createNewBoard()
	{
		// Removing any existing DrawingView
		cleanUpExistingView();
 
		// Making the drawing layout visible
		Activity parentActivity = getActivity();
		LinearLayout drawingLayout = (LinearLayout) parentActivity.findViewById(R.id.layout_drawing);
		parentActivity.findViewById(R.id.textview_welcome).setVisibility(View.GONE);
		drawingLayout.setVisibility(View.VISIBLE);
 
		// Adding DrawingView to the DrawingLayout
		mDrawingView = new DrawingView(parentActivity);
		mDrawingView.setBackgroundResource(R.drawable.chalkboard);
		drawingLayout.addView(mDrawingView);
	}
 
	/**
	 * Removing any existing DrawingView
	 */
	private void cleanUpExistingView()
	{
		Activity parentActivity = getActivity();
 
		// Resetting the drawing title's text
		((EditText)parentActivity.findViewById(R.id.edittext_drawing)).setText("");
 
		if (mDrawingView != null) 
		{
			// Remove DrawingView from DrawingLayout
			LinearLayout drawingLayout = (LinearLayout) parentActivity.findViewById(R.id.layout_drawing);
			drawingLayout.removeView(mDrawingView);
 
			// Cleaning up the background as well
			Drawable backgroundDrawable = mDrawingView.getBackground();
			if(backgroundDrawable != null)
			{
				backgroundDrawable.setCallback(null);
				mDrawingView.setBackgroundResource(0);
				mDrawingView.destroyDrawingCache();
			}
 
			mDrawingView = null;
		}
	}
  • Now, just call createNewBoard() method from the click callback of 'Create New' button (for the case of TABLETS):
MainActivity.java
	// Let's create a new board.
	fragmentTwo.createNewBoard();
	Toast.makeText(this, "New board created.", Toast.LENGTH_SHORT).show();

Step 04: [TABLETS] Save the drawing

This means we have to implement the click callback for the 'Save' button. Since for Tablets, 'Save' button is on Main Screen, therefore, its onButtonClick() callback implementation should be on Main screen (i.e. MainActivity class) as well. Also, as you already know, Fragment is used to make design modular, so that it could be reused at multiple places. This is why the main implementation for such a method should lie in FragmentTwo class rather than MainActivity.

  • So, let's implement saveDrawing() method in FragmentTwo class.
FragmentTwo.java
	public static final String PREFS = "com.example.board.drawings";
 
	/**
	 * Method to save the drawing in SharedPreferences.
	 */
	public void saveDrawing()
	{
		Activity parentActivity = getActivity();
 
		// Get drawing title from the EditText
		String drawingName =  ((EditText)parentActivity.findViewById(R.id.edittext_drawing)).getText().toString();
 
		// Prompt user to give a drawing name if he/she has not already entered
		if(drawingName == null || drawingName.isEmpty())
		{
			Toast.makeText(parentActivity, "Please enter a name for your drawing", Toast.LENGTH_SHORT).show();
			return;
		}
 
		if(mDrawingView == null)
		{
			Toast.makeText(parentActivity, "No drawing found!", Toast.LENGTH_SHORT).show();
			return;
		}
 
		// This is where our drawing is stored
		ArrayList<ArrayList<Float>> overallDrawingList = mDrawingView.mOverallDrawingList;
		String flattenedDrawingListString = "";
 
		// Get number of lines
		int numParts = overallDrawingList.size();
 
		// Store the drawing as a FLAT STRING in SharedPreferences
		SharedPreferences preferences = parentActivity.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
		SharedPreferences.Editor editor = preferences.edit();
 
		// Go through every line of the drawing
		for (int i = 0; i < numParts; i++) 
		{
			ArrayList<Float> partDrawingList = overallDrawingList.get(i);
 
			int numPoints = partDrawingList.size();
			for (int j = 0; j < numPoints;) 
			{
				flattenedDrawingListString += partDrawingList.get(j++);
 
				if (j < numPoints)
					flattenedDrawingListString += ",";
			}
 
			// Separate strings representing a line by a tab space ('\t')
			flattenedDrawingListString += "\t";
		}
 
		// Store the generated string and commit the changes.
		editor.putString(drawingName, flattenedDrawingListString);
		editor.commit();
 
		Toast.makeText(parentActivity, "'" + drawingName + "' saved successfully.", Toast.LENGTH_SHORT).show();
	}
  • Let's add a case for the'Save' button in existing onButtonClick() method in MainActivity, and call saveDrawingMethod on FragmentTwo object.
MainActivity::onButtonClick()
		case R.id.btn_two_save:
			fragmentTwo.saveDrawing();
			break;

And, at this point, our app can save the drawings! Next: retrieve these drawings.

Step 05: [TABLETS] Open Existing Drawings

In this step, we would first like to see a list of already existing drawings. We can use ContextMenu for this purpose, as shown in the figure below:

  • To create a ContextMenu, we first have to register it (via registerForContextMenu() method) with the UI control, which could then trigger it (via openContextMenu() method) later on. So, let's register for this context menu in onCreate() method itself, as it is the best place to initialize your UI.
MainActivity::onCreate()
	View openExistingButtonView = findViewById(R.id.btn_one_open_existing);
	registerForContextMenu(openExistingButtonView);	
  • Now, just call openContextMenu() method in the click callback of 'Open Existing' button.
MainActivity::onButtonClick()
		case R.id.btn_one_open_existing:
			openContextMenu(findViewById(R.id.btn_one_open_existing));
			break;
  • BUT.. BUT.. BUT.., we missed specifying what will show up when this context menu is created. We do this through onCreateContextMenu() method. Since, we're storing the drawings in SharedPreferences, we the name of already existing drawings are basically the value of the keys of all the items in SharedPreferences. So, here's how we can implement this method:
MainActivity::onCreateContextMenu()
	@Override
	public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo)
	{
		// Get all the drawing names.
		SharedPreferences preferences = getSharedPreferences(FragmentTwo.PREFS, Context.MODE_PRIVATE);
		Map<String, ?> prefs = preferences.getAll(); // Question mark stands for a separate representative
 
		if(prefs.isEmpty())
		{
			Toast.makeText(this, "Sorry, no boards present!", Toast.LENGTH_SHORT).show();
			return;
		}
 
		// Setting title for ContextMenu
		menu.setHeaderTitle(getResources().getString(R.string.one_select_board));
 
		// Iterating through all the items in SharedPreferences
		for(Map.Entry<String, ?> entry : prefs.entrySet()) 
		{
			// Extracting the key, which is actually a drawing name.
		    String key = entry.getKey().toString();
 
		    // Add them to menus.
		    menu.add(key);
		}
	}
  • Now, we want that if a user clicks on any drawing name showing up in the Context Menu, we should bring up that particular drawing back on the board. Fortunately, to have a click callback for an item in a ContextMenu, we just need to implement onContextItemSelected() method.
MainActivity::onContextItemSelected()
	@Override
	public boolean onContextItemSelected(MenuItem item)
	{
		// Get the drawing name
		String drawingName = item.getTitle().toString();
 
		// Check again, if this is a PHONE or a TABLET.
		FragmentTwo fragmentTwo = (FragmentTwo) getFragmentManager().findFragmentById(R.id.fragment_two);
		if (fragmentTwo == null)	// PHONE!
		{
			// [Hint] Need to launch DrawingActivity and pass on extra parameters
			// to tell DrawingActivity to execute openExistingBoard() method.
			Toast.makeText(this, "Will implement later.", Toast.LENGTH_SHORT).show();
		}
		else				// TABLET!
		{
			// FragmentTwo is in the same Activity. Update its UI to show the corresponding drawing.
			fragmentTwo.openExistingBoard(drawingName);
		}
 
        return super.onContextItemSelected(item);
	}
  • And, we come to our last step, where we'll implement openExistingBoard() method. In this method, we'll extract the 'flat string' from SharedPreferences and unfold it to show up as a drawing on the board. Here's is its implementation:
FragmentTwo.java
	/**
	 * Method to open an existing drawing board.
	 * 
	 * @param drawingName	Drawing Title given by user earlier
	 */
	public void openExistingBoard(String drawingName)
	{
		cleanUpExistingView();
 
		ArrayList<ArrayList<Float>> alreadyStoredDrawingList = new ArrayList<ArrayList<Float>>();
		alreadyStoredDrawingList = readAlreadyStoredDrawingList(drawingName);
 
		// If there is at least one point in the drawing
		if (alreadyStoredDrawingList != null && !alreadyStoredDrawingList.isEmpty())
		{
			Activity parentActivity = getActivity();
			LinearLayout drawingLayout = (LinearLayout) parentActivity
					.findViewById(R.id.layout_drawing);
 
			if (mDrawingView == null)
			{
				mDrawingView = new DrawingView(parentActivity);
				mDrawingView.setBackgroundResource(R.drawable.chalkboard);
				drawingLayout.addView(mDrawingView);
			}
 
			if (mDrawingView != null)
			{
				mDrawingView.setOverallDrawingList(alreadyStoredDrawingList);
				mDrawingView.invalidate();
			}
 
			parentActivity.findViewById(R.id.textview_welcome).setVisibility(View.GONE);
			drawingLayout.setVisibility(View.VISIBLE);
			((EditText) parentActivity.findViewById(R.id.edittext_drawing)).setText(drawingName);
		}
	}
 
	/**
	 * Helper method to extract already store drawing list from
	 * SharedPreferences in a specific format.
	 */
	private ArrayList<ArrayList<Float>> readAlreadyStoredDrawingList(String drawingName)
	{
		ArrayList<ArrayList<Float>> drawingList = new ArrayList<ArrayList<Float>>();
 
		Activity parentActivity = getActivity();
		if (parentActivity != null)
		{
			SharedPreferences preferences = parentActivity.getSharedPreferences(FragmentTwo.PREFS,
					Context.MODE_PRIVATE);
			String flattenedDrawingList = preferences.getString(drawingName, null);
 
			if (flattenedDrawingList != null)
			{
				// Check if the list is empty
				if (flattenedDrawingList.isEmpty())
					return drawingList;
 
				String drawingLines[] = flattenedDrawingList.split("\t");
 
				// Unfolding the flat string
				for (int i = 0; i < drawingLines.length; i++)
				{
					String linePoints[] = drawingLines[i].split(",");
 
					ArrayList<Float> linesList = new ArrayList<Float>();
 
					for (int j = 0; j < linePoints.length; j++)
						linesList.add(Float.parseFloat(linePoints[j]));
 
					drawingList.add(linesList);
				}
			}
		}
 
		return drawingList;
	}

Source Code

Available at Github.


AlertDialog

  • Looks something like this-

  • Can be easily created using AlertDialogBuilder
// 1. Instantiate an AlertDialog.Builder with its constructor
	AlertDialog.Builder builder = new AlertDialog.Builder(this /* Activity Context */);
 
// 2. Chain together various setter methods to set the dialog characteristics
	builder.setMessage("Are you sure?")
	       .setTitle("Delete Confirmation");
 
	// Add the buttons
	builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() 
		{
		   public void onClick(DialogInterface dialog, int id) 
		   {
		       // User clicked "Yes" button
		   }
	       });
	builder.setNegativeButton("No", new DialogInterface.OnClickListener() 
		{
		   public void onClick(DialogInterface dialog, int id) 
		   {
			// User clicks "No" button, which may mean cancellation of the dialog
			/* dialog.cancel(); */
		   }
	       });
 
// 3. Create and show
	builder.create();
	builder.show();	

Class Exercise

1. Add a “Delete Existing” button to the left pane.

2. Clicking on this “Delete Existing” Button should show up the context menu with list of existing drawings.

3. Clicking on an item in the context menu should show a prompt to user through an AlertDialog to check if =the user really wants to delete that drawing.

4. Clicking on the positive button ('Yes') should delete drawing from Shared Preferences.


Homework Assignment

Make the Board app working for PHONE. One should be able to (1) create a new board, (2) make drawing on it, (3) save that drawing, (4) open an existing drawing and (5) delete an existing drawing.

android-labs-s14/class-06.txt · Last modified: 2014/02/19 14:14 by prakhar