In this class, we'll be learning about:
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
:
<?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>
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.
/** * 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.
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:
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:
@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:
@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(); } }
In this step, we want to bring up a clean drawing board whenever user clicks on 'Create New' button.
drawable
inside res
folder, and copy the downloaded file to that folder.DrawingView
to FragmentTwo, we'll implement a method createNewBoard()
in FragmentTwo
class, which will be called when user clicks on 'Create New' button.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; } }
createNewBoard()
method from the click callback of 'Create New' button (for the case of TABLETS):// Let's create a new board. fragmentTwo.createNewBoard(); Toast.makeText(this, "New board created.", Toast.LENGTH_SHORT).show();
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
.
saveDrawing()
method in FragmentTwo
class.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(); }
onButtonClick()
method in MainActivity
, and call saveDrawingMethod
on FragmentTwo
object.case R.id.btn_two_save: fragmentTwo.saveDrawing(); break;
And, at this point, our app can save the drawings! Next: retrieve these 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:
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.View openExistingButtonView = findViewById(R.id.btn_one_open_existing); registerForContextMenu(openExistingButtonView);
openContextMenu()
method in the click callback of 'Open Existing' button.case R.id.btn_one_open_existing: openContextMenu(findViewById(R.id.btn_one_open_existing)); break;
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:@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); } }
ContextMenu
, we just need to implement onContextItemSelected()
method.@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); }
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:/** * 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; }
Available at Github.
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();
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.
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.