User Tools

Site Tools


android-labs-s14:class-05

This is an old revision of the document!


Class 05

In this class, we'll be learning about:

  • Fragments
  • Canvas, Bitmap, Paint, Path
  • Context Menu

Demo-1

We'll see a small demo of the app we're going to build next.

Fragments

  • A Fragment represents a behavior or a portion of user interface in an Activity.
  • Fragments enable more modular activity design, making it easier to adapt an application to different screen orientations and multiple screen sizes.
  • Fragments must be embedded in activities; they cannot run independent of activities.
  • You can combine multiple fragments in a single activity to build a multi-pane UI and reuse a fragment in multiple activities.

  • You can think of a fragment as a modular section of an activity, which has its own lifecycle, receives its own input events, and which you can add or remove while the activity is running (sort of like a “sub activity” that you can reuse in different activities).

Exercise

Let's build this Board application.

This app will be using two fragments:

  1. Pane for Buttons
  2. Pane for Canvas

Step 00: Create Android Project (Min API Level: 11)

Start by creating an Android Project with minimum API Level = 11, as Fragment is available only on or after that. To use Fragments for API Level < 11, you have to use a compatibility package provided by Android. See Support Library for more details.

Step 01: UI for the fragments

Now, create two Android XML files in res/layout as UI for the two fragments:

  1. fragment_01.xml : Having two buttons inside a Linear/Relative Layout
  2. fragment_02.xml : Having a TextView in the center
fragment_01.xml
<?xml version="1.0" encoding="utf-8"?>
 
<RelativeLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/grey" >
 
    <Button
        android:id="@+id/btn_one_create_new"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/one_create_new"
        android:layout_alignParentTop="true"
        android:layout_marginTop="@dimen/btn_margin_vertical"
        android:layout_marginBottom="@dimen/btn_margin_vertical"
        android:layout_marginLeft="@dimen/btn_margin_horizontal"
        android:layout_marginRight="@dimen/btn_margin_horizontal"
        android:onClick="onButtonClick" />
 
    <Button 
        android:id="@+id/btn_one_open_existing"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/one_open_existing"
        android:layout_below="@id/btn_one_create_new"
        android:layout_marginTop="@dimen/btn_margin_vertical"
        android:layout_marginBottom="@dimen/btn_margin_vertical"
        android:layout_marginLeft="@dimen/btn_margin_horizontal"
        android:layout_marginRight="@dimen/btn_margin_horizontal"
        android:onClick="onButtonClick" />
 
</RelativeLayout>
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" >
 
    <!-- Welcome Title -->
   	<TextView
       android:id="@+id/textview_welcome"
       android:layout_height="match_parent"
       android:layout_width="match_parent"
       android:gravity="center"
       android:text="@string/two_welcome"
       android:textSize="@dimen/textsize_welcome_title"
       android:textStyle="bold" />
 
</LinearLayout>

You must be observing errors in these xml files as some of the values in them are not yet defined. We've to define them in different xml files in res/values directory.

colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- Feel free to play with the values here -->
 
    <color name="light_grey">#E0E0E0</color>
    <color name="grey">#D0D0D0</color>
 
</resources>
dimens.xml
<resources>
    <!-- Feel free to play with the values here -->
 
    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="activity_horizontal_margin">16dp</dimen>
    <dimen name="activity_vertical_margin">16dp</dimen>
 
    <dimen name="btn_margin_vertical">30dp</dimen>
    <dimen name="btn_margin_horizontal">10dp</dimen>
 
    <dimen name="textsize_welcome_title">24sp</dimen>
 
</resources>
strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- Feel free to play with the values here -->
 
    <string name="app_name">Board</string>
    <string name="action_settings">Settings</string>
    <string name="hello_world">Hello world!</string>
 
    <string name="one_create_new">Create New</string>
    <string name="one_open_existing">Open Existing</string>
    <string name="one_select_board">Select a Board</string>
 
    <string name="two_welcome">Welcome to Board!</string>
    <string name="two_drawing">Name Your Drawing</string>
 
</resources>

Step 02: Add the above fragments to Main Screen

For Phones

For phones, we only want the fragment (fragment_01.xml) with the buttons on the Main Screen.

Hence, Main screen layout, which is layout/activity_main.xml, should look something like below:

layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
 
<!-- PHONE -->
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:baselineAligned="false"
    android:background="@color/light_grey" >
 
    <!-- Fragment One only -->
   	<fragment
        android:id="@+id/fragment_one"
        android:name="com.example.board.FragmentOne"
        android:layout_height="match_parent"
        android:layout_width="0dp"
        android:layout_weight="1" />
 
</LinearLayout>
For Tablets

In case of a Tablet device, we want both fragments to show up in the main screen.

  • To have a different layout for tablets, we have to add another folder layout-large inside res folder (See 'Using Configuration qualifiers' section for more details).
  • Now, create an Android xml file with the same name as used for Main screen xml file (i.e., activity_main.xml), which should have both the fragments inside a Linear/Relative Layout.
layout-large/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
 
<!-- TABLET -->
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:baselineAligned="false"
    android:background="@color/light_grey" >
 
   <!-- Fragment One -->
   <fragment
        android:id="@+id/fragment_one"
        android:name="com.example.board.FragmentOne"
        android:layout_height="match_parent"
        android:layout_width="0dp"
        android:layout_weight="1" />
 
   <!-- Fragment Two -->
   <fragment
        android:id="@+id/fragment_two"
        android:name="com.example.board.FragmentTwo"
        android:layout_height="match_parent"
        android:layout_width="0dp"
        android:layout_weight="3" />
 
</LinearLayout>

Step 03: Create Fragment classes for both the fragments

You may have noticed that inside the <fragment> xml tag in activity_main.xml file, we have specified a class that will represent this fragment. So, let's create those classes -

  1. FragmentOne.java: corresponding to fragment_01.xml
  2. FragmentTwo.java: corresponding to fragment_02.xml
  • Create classes FragmentOne.java and FragmentTwo.java inside src/com.example.board package, extending Fragment class. Both of these classes should implement onCreateView() method, as shown below:
	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
	{
		return inflater.inflate(R.layout.<CORRESPONDING_LAYOUT>, container, false);
	}

A little background on onCreateView() method:

The system calls **onCreateView()** method when it's time for the fragment to draw its user interface for the first time. 
To draw a UI for your fragment, you must return a View from this method that is the root of your fragment's layout.
You can return null if the fragment does not provide a UI.

Now, if at this point, you launch your App on your device/emulators, you should see our basic UI skeleton on their screens.

Step 04: [Phone Only] Create another screen

  • On Phone, we need to have another screen containing fragment_02.xml as its UI. So, for that, first create an Android XML file activity_drawing.xml inside res/layout folder. This xml file will only include fragment_02 inside a LinearLayout.
activity_drawing.xml
<?xml version="1.0" encoding="utf-8"?>
 
<!-- PHONE -->
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:baselineAligned="false"
    android:background="@color/light_grey" >
 
    <!-- Fragment Two only -->
    <fragment
        android:id="@+id/fragment_two"
        android:name="com.example.board.FragmentTwo"
        android:layout_height="match_parent"
        android:layout_width="0dp"
        android:layout_weight="1" />
 
</LinearLayout>
  • Now, create a class DrawingActivity.java inside src/com.example.board, extending Activity class. Set the content view of DrawingActivity class to activity_drawing.xml.
  • Again, don't forget to register DrawingActivity.java in AndroidManifest.xml.

Step 04: Implement Button Callbacks

Now, we just need to implement onButtonClick callbacks for the two buttons on Main screen. On Phone, clicking on 'Create New' button should lead us to DrawingActivity, whereas on Tablet, it should just show up a Toast Message (for now).

To identify, whether the application is being run on Phone or Tablet, just look out for presence of ''FragmentTwo'' object in ''MainActivity'' class.
In case of Tablet, ''FragmentTwo'' object would have been initialized as it is part of its Main screen UI.

So, onButtonClick method can be implemented as follows:

onButtonClick()
	public void onButtonClick(View v)
	{
		FragmentTwo fragmentTwo = (FragmentTwo) getFragmentManager().findFragmentById(R.id.fragment_two);
 
		switch(v.getId())
		{
		case R.id.btn_one_create_new:
 
			if (fragmentTwo == null)	// PHONE!
			{
				// Need to launch another activity
				Intent i = new Intent(this, DrawingActivity.class);
				startActivity(i);
			}
			else				// TABLET!
			{
				// Do nothing right now, and just show up a toast.
				Toast.makeText(this, "Will be implemented later", Toast.LENGTH_SHORT).show();
			}
 
			break;
 
		case R.id.btn_one_open_existing:
 
			Toast.makeText(this, "Will be implemented later", Toast.LENGTH_SHORT).show();
 
			break;
 
		default:
		}
	}

Source Code

Available at Github.


Demo-02

We'll see a small demo of the app we're going to build next.

Adding a drawing board to the app

Exercise

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, 0, null);
		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, Bitmap)
  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 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, search for a background .png (or .jpg) image of a chalkboard, or you can download it from here. Rename it to chalkboard.png (or chalkboard.jpg), if not already.
  • 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)
	{
		// Setting title for ContextMenu
		menu.setHeaderTitle(getResources().getString(R.string.one_select_board));
 
		// Get all the drawing names.
		SharedPreferences preferences = getSharedPreferences(FragmentTwo.PREFS, Context.MODE_PRIVATE);
		Map<String, ?> prefs = preferences.getAll();
 
		// 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 click 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.


Programming Assignment

Make the Board app working for phone. One should be able to create a new board, make drawing, save drawing, and open an existing board on Phone.

android-labs-s14/class-05.1392662989.txt.gz · Last modified: 2014/02/17 12:49 by prakhar