Cool Clock - Google URL Shortener

5 downloads 188 Views 2MB Size Report
Least-squares data/curve fitting. Spring force graph layout of UML-like. OO diagram showing classes & interrelations
Cool Clock Helping children to read analog clocks

An introduction to developing custom 2D graphics Android applications Richard Creamer 2to32minus1 (at) gmail (dot) com

Links Home Page LinkedIn Profile Android Market Cool Clock Product Listing External Cool Apps Software Website External Cool Clock Product Details Page

1 Copyright (c) Richard Creamer 2011 - All Rights Reserved

What are 2D Custom Graphics Components?

Below are several examples of 2D components previously developed by the author in Java/Swing:

2 Copyright (c) Richard Creamer 2002 - 2011 - All Rights Reserved

What are 2D Custom Graphics Components?

Below are several examples of 2D components previously developed by author in Java/Swing:

Custom keypad

“Gel” button

Process Editor

Joystick for games, robotics

Least-squares android:title="@string/hide_circular_arrows_title" android:summary="@string/hide_circular_arrows_summary" android:defaultValue="true" />

Clock\res\values\strings.xml Excerpt Hide Circular Arrows 19 Copyright (c) Richard Creamer 2011 - All Rights Reserved

Creating Main Menus

20

Creating Main Menus CoolClock.java:

2

Handle Main Menu Creation

1

Define Main Menu GUI

@Override public boolean onCreateOptionsMenu( Menu menu ) { super.onCreateOptionsMenu( menu ); // Call base class first MenuInflater inflater = getMenuInflater(); inflater.inflate( R.menu.menu, menu ); return true; } CoolClock.java:

3

Handle Main Menu Button Presses

Clock\res\menu\menu.xml



@Override public boolean onOptionsItemSelected( MenuItem mi ) { super.onOptionsItemSelected( mi ); // Called first switch ( mi.getItemId() ) { case R.id.help: startActivity( new Intent( this, HelpActivity.class ) ); return true; case R.id.settings: startActivityForResult( new Intent( this, PrefsActiviy.class ), PREFS_ACTIVITY_ID ); return true; case R.id.about: startActivity( new Intent( this, AboutActivity.class ) ); return true; About Menu “Inflation” case R.id.exit: In this line of code: finish(); inflater.inflate( R.menu.menu, menu ); return true; The MenuInflater reads menu.xml and then } programmatically adds the MenuItems to return false; the method argument: menu }

Copyright (c) Richard Creamer 2011 - All Rights Reserved

Result: Main Menu Screenshot

21

Introduction to Android Preferences

22

Android Preferences Overview • Android includes an easy-to-use Preferences sub-system that allows end users to customize an application’s settings. • Preferences are roughly equivalent to ToolsOptions for desktop applications.

Pressing the Settings button displays the Preferences/Settings menu

The main menu is displayed when the user presses the device’s Menu button

The Preferences screen GUI layout/content is defined in settings.xml ( see slide 25 ) 23 Copyright (c) Richard Creamer 2011 - All Rights Reserved

Android Preferences • The Android Preferences sub-system provides: • XML-based declarative GUI layout definition of Preferences menus/screens • Automatic launching of Preferences screens and their lifecycle management • Transparent persistence of user changes • Easy-to-use API for retrieving persisted Preferences parameter values • The Cool Clock Preferences screens: Primary Preferences Screen

Clock Mode Options

Minute Display Mode Options

Hand Linked to Circular Arrows Options

24 Copyright (c) Richard Creamer 2011 - All Rights Reserved

settings.xml ( Defines Preferences GUI layout and content* )







*Multiple-choice widget values are stored in res\values\arrays.xml

25 Copyright (c) Richard Creamer 2011 - All Rights Reserved

How Preferences Menus Work - Details User

Presses

In main activity ( CoolClock.java ) public boolean onCreateOptionsMenu( Menu menu ) { super.onCreateOptionsMenu( menu ); MenuInflater inflater = getMenuInflater(); inflater.inflate( R.menu.menu, menu ); return true; }

Calls (1st time)

Android

( R.menu.menu )

Phone Menu button

Displays

Android

Main menu

Presses

User

Settings button

Calls

Android

Time Displays

Android User

The “inflater” reads the defined main menu items defined in menu.xml and programmatically adds these menu items to the method argument’s Menu object

Preferences screen

Interacts with

User Android

Closes

In main activity ( CoolClock.java ) public boolean onOptionsItemSelected( MenuItem mi ) { switch ( mi.getItemId() ) { ... cases for other main menu items ... case R.id.settings: startActivityForResult( new Intent( this, PrefsActivity.class ), PREFS_ACTIVITY_ID ); return true; ... Tells Android to launch } Preferences screen return false; }

Preferences screen

Preferences screen Calls

In main activity ( CoolClock.java ) protected void onActivityResult( int requestCode, int resultCode, Intent encoding="utf-8"?>

Note nesting of GUI components

android.widget.TextView TextView extends android.view.View

2 Define activity class

AboutActivity.java package net.maxuint32.coolapps; import android.app.Activity; import android.os.Bundle;

Activity’s GUI layout

public class AboutActivity extends Activity { @Override protected void onCreate( Bundle savedInstanceState ) { super.onCreate( savedInstanceState ); setContentView( R.layout.about ); } } strings.xml (excerpt) \ CoolClock v0.8\n\ Copyright (c) Richard Creamer 2011\n\ All Rights Reserved\n\ Email: [email protected]\n\n\ CoolClock can run in three modes:\n\n\ 1. Learn Mode:\n\n\ This mode helps teach children how to read an analog clock.\n\n\ Children can drag clock hands and watch:\n\n\ - The other hands move\n\ - How the digital time changes\n\ - How the "phrase" time changes\n\ - \"Before\" and \"after\" circular arrows\n\n\ 2. Clock Mode:\n\n\ In this mode, CoolClock is just a normal analog clock - it displays the current time and runs.\n\n\ 3. Modern Clock:\n\n\ In this mode, a more modern clock is drawn, without numbers or minute ticks.

3

Start activity

CoolClock.java (excerpt) @Override public boolean onOptionsItemSelected( MenuItem mi ) { ... switch ( mi.getItemId() ) { case R.id.about: startActivity( new Intent( this, AboutActivity.class ) ); return true; } .... Here we are starting the return false; } AboutActivity when a main

4 Define text content

Copyright (c) Richard Creamer 2011 - All Rights Reserved

menu item is pressed, but Activities can be started at any time.

29

Custom 2D Graphical Components

30

A Basic Custom 2D Graphics App/Component • GraphicsTest1 is a very simple custom-drawn 2D component • It draws a gradient background with a random number of spheres of random color and opacity • It ignores many best practices such as avoiding expensive computations in the GUI thread

31 Copyright (c) Richard Creamer 2011 - All Rights Reserved

GraphicsTest1 - Salient Java Source Code Elements package ... import ...

Steps for creating a custom 2D component Activity

1. 2. 3. 4.

public class GraphicsTest1 extends Activity { ... attributes... @Override public void onCreate( Bundle savedInstanceState ) { super.onCreate( savedInstanceState ); requestWindowFeature( Window.FEATURE_NO_TITLE ); setContentView( new GraphicsView( this ) ); } 4

Create new class which extends android.view.View New class must override onDraw() onDraw() does all drawing Call Activity.setContentView( custom 2D component )

GraphicsTest1 sets its content view to an instance of GraphicsView

// Nested class that fills its bounds with random spheres private static class GraphicsView extends View {

1

... attributes... public GraphicsView( Context context ) { super( context ); }

2

GraphicsView overrides onDraw()

@Override protected void onDraw( Canvas canvas ) { // Do all drawing Do all drawing: }

} // End nested GraphicsView class

background + spheres/graphics

3

Note: The GraphicsView class does not have to be a nested class - it was only done this way so the entire Activity + View would fit into a single Java file...

} // End GraphicsTest1 class 32 Copyright (c) Richard Creamer 2011 - All Rights Reserved

GraphicsTest1 - Complete Java Source Code package org.example.graphics; import java.util.Random; import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.view.View; import android.view.Window; import android.graphics.Canvas; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.RadialGradient; import android.graphics.Shader; public class GraphicsTest1 extends Activity {

3 Do all drawing: gradient paint background + spheres

GraphicsTest1 sets its content view to be an instance of GraphicsView

@Override public void onCreate( Bundle savedInstanceState ) { super.onCreate( savedInstanceState ); requestWindowFeature( Window.FEATURE_NO_TITLE ); setContentView( new GraphicsView( this ) ); }

4

private void drawRandSpheresAndBkg( Canvas canvas ) { int numCircles = rand.nextInt( maxCircles ); if ( numCircles < minCircles ) numCircles = minCircles; int w = this.getWidth(); int h = this.getHeight(); Paint bkgPaint = new Paint(); bkgPaint.setShader( new LinearGradient( 0, 0, w, h, c1, c2, Shader.TileMode.MIRROR ) ); canvas.drawPaint( bkgPaint ); // This fills Canvas with background gradient paint for ( int i = 0; i < numCircles; ++i ) { int xc = rand.nextInt( w ); Such methods can be computationally int yc = rand.nextInt( h ); expensive and should not be done in the int r = rand.nextInt( ( w + h )/16 ); main GUI thread! Run expensive tasks int red = rand.nextInt( 255 ); in another thread! Then, post results int green = rand.nextInt( 255 ); back to GUI thread using a Handler. int blue = rand.nextInt( 255 ); int alpha = rand.nextInt( 255 ); int color = Color.argb( alpha, red, green, blue ); drawSphereImage( canvas, xc, yc, r, color ); // Draw the sphere } }

public void drawSphereImage( Canvas canvas, float xc, float yc, float radius, int color ) { if ( radius < 3.0f ) radius = 3.0f; // Nested class that fills its bounds with random spheres Paint paint = new Paint( Paint.ANTI_ALIAS_FLAG ); Class should final float highlightOffsetInPct = 0.4f; 1 private static class GraphicsView extends View { extend View float highlightOffset = highlightOffsetInPct * radius; float highlightXc = xc - highlightOffset; private final int maxCircles = 150, minCircles = 5; float highlightYc = yc - highlightOffset; private final int c1 = 0xff4466aa, c2 = 0xff112244; final float radiusScaleFactor = 3; private final Random rand = new Random( System.currentTimeMillis() ); float highlightRadius = radiusScaleFactor * highlightOffset; RadialGradient rg = new RadialGradient( highlightXc, highlightYc, public GraphicsView( Context context ) { highlightRadius, 0xffffffff, color, Shader.TileMode.MIRROR ); super( context ); GraphicsView paint.setShader( rg ); } overrides canvas.drawCircle( xc, yc, radius, paint ); onDraw() 2 } @Override } // End GraphicsView class protected void onDraw( Canvas canvas ) { } // End GraphicsTest1 class drawRandSpheresAndBkg( canvas ); }

33 Copyright (c) Richard Creamer 2011 - All Rights Reserved

Custom 2D View Components - Overview Creating the basic GraphicsTest1 class was pretty simple: • Create a class that extends android.view.View ( GraphicsView ) • GraphicsView must override View.onDraw( Canvas canvas ) • In onDraw(), perform all the necessary drawing ( images, shapes, primitives ) • Main Activity calls setContentView() with an instance of GraphicsView

When creating non-trivial components, other details become important, including: • Never allow direct access to a GUI’s state from a non-GUI thread ( thread-safe design ) • Appropriately respond to lifecycle events • Use background/worker threads to perform long-running, computationally-expensive tasks • Methods to return results from non-GUI threads to the main GUI thread include: • Call Activity.runOnUiThread( Runnable ) • Call the android.os.Handler.post( Runnable ) method of a Handler object • The Handler must be instantiated in the GUI thread • If user touch input is required the View-derived class should: • Implement the OnTouchListener interface • Implement View.onTouch() • Handle all touch events and object hit testing if applicable • Minimize use of system resources • Call base class methods at the appropriate point when overriding methods 34 Copyright (c) Richard Creamer 2011 - All Rights Reserved

Handling Touch Events

35

Implement OnTouchListener.onTouch() public boolean onTouch( View v, MotionEvent me ) { if ( curPrefs.clockOperationMode != E.ClockOperationMode.LearnMode ) return false; if ( !curPrefs.enableHandDragging || me.getPointerCount() > 1 ) // Only handle 1 finger at this time return false; float x = me.getX(); float y = me.getY(); int action = me.getAction() & MotionEvent.ACTION_MASK;

ClockPanel implements the OnTouchListener interface in order to receive touch input

switch ( action ) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: E.ClockHands hand = isCloseToHand( x, y ); if ( hand == E.ClockHands.None ) return false; inDrag = true; dragHand = hand; return true;

case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: inDrag = false; dragHand = E.ClockHands.None; return true; case MotionEvent.ACTION_MOVE: processMove( x, y ); return true;

Begin a hand drag...

Terminate a hand drag...

Move the current drag hand...

} return false; }

36 Copyright (c) Richard Creamer 2011 - All Rights Reserved

Graphics techniques used in Cool Clock

37

Cool Clock’s Graphics Layers Background LinearGradient shader mirroring = true

Fill clock oval

Fill clock oval, clipped, with LinearGradient shader

Add hour and minute numbers + minute ticks

( This is what is cached )

• Hands (LinearGradient shader) • Hands are drawn via Path objects • Center dot (RadialGradient shader)

Upper-left highlight (LinearGradient)

Lower-right highlight (LinearGradient)

Digital time, Phrase time

• Circular arrows fill/edges • Circular arrow labels

Copyright (c) Richard Creamer 2011 - All Rights Reserved

38

Utility Class: Vector2d (selected methods) //Copyright (c) Richard Creamer 2002 - 2011 - All Rights Reserved

Complete Vector2d public interface:

package net.maxuint32.coolapps; import android.graphics.PointF;

public float x, y; public final class Vector2d { public Vector2d(); public float x, y; public Vector2d( final float x, final float y ); public Vector2d( final float x, final float y ) { public Vector2d( final Vector2d v ); this.x = x; public Vector2d( final PointF p1, final PointF p2 ); this.y = y; public Vector2d setComponents( final float x,final float y ); } public float length(); public synchronized Vector2d rotateClockwise( final float angle ) { public float dotProduct( final Vector2d v ); rotateCounterClockwise( -angle ); public float cosAngle( final Vector2d v ); return this; public Vector2d zero(); } public Vector2d add( final Vector2d v ); public synchronized Vector2d rotateCounterClockwise( final float angle ) { float cos = ( float ) Math.cos( angle ); public Vector2d mult( final float factor ); float sin = ( float ) Math.sin( angle ); public Vector2d div( final float quotient ); CG Rotation Matrix float newX = cos * x + sin * y; public Vector2d rotateClockwise( final float angle ); float newY = -sin * x + cos * y; x’ x public Vector2d rotateCounterClockwise( final float angle ); cos ѳ sin ѳ x = newX; public Vector2d setLength( final float length ); y’ -sin ѳ cos ѳ y y = newY; public Vector2d normalize(); return this; public Vector2d parallelOfLength( final float length ); } public synchronized Vector2d setLength( final float length ) { normalize(); x = x * length; y = y * length; return this; } public synchronized Vector2d normalize() { float length = length(); if ( length == 0f ) throw new IllegalStateException( "Vector length is zero! Cannot divide by zero!" ); x = x / length; y = y / length; return this; } public synchronized float length() { return ( float ) Math.sqrt( x * x + y * y ); } 39 } Copyright (c) Richard Creamer 2002 - 2011 - All Rights Reserved

=

Drawing Shapes Using Paths 1. 2. 3. 4. 5. 6.

Create a Path object p ( see below for details ) Call p.moveTo() for starting point 0 Call p.lineTo() or p.arcTo() points 1 - 9 in clockwise order Call p.close() Configure canvas paint or shader ( can be fill or stroke mode ) Call canvas.drawPath( p )

0

1

2

3

4

8

7 9 private Path getRhsArrow( float a1, float a2 ) { // Almost complete & accurate... Yellow arrows are Vector2d float r1 = Innermost arrowhead side point radius 6 rotation angles float r2 = Annulus inner radius 5 float r3 = Annulus center radius float r4 = Annulus outer radius float r5 = Outermost arrowhead side point radius float deltaAInDeg = toDegrees( F.abs( a1 - a2 ) ); ( xc, yc ) Use Vector2d class Path p = new Path(); to handle math Vector2d v = new Vector2d( 0, -r3 ); ( previous slide ) p.moveTo( xc + v.x, yc + v.y ); v.rotateClockwise( arrowheadAngle ).setLength( r5 ); p.lineTo( xc + v.x, yc + v.y );  method = Path methods v.setLength( r4 );  method = Vector2d methods p.lineTo( xc + v.x, yc + v.y ); p.arcTo( new RectF( xc - r4, yc - r4, xc + r4, yc + r4 ), -90 + arrowheadAngle, deltaAInDeg - 2f * arrowheadAngle ); p.lineTo( xc + v.x, yc + v.y ); v.rotateClockwise (arrowheadAngle ).setLength( r3 ); // We can chain method calls because most Vector2d methods return this p.lineTo( xc + v.x, yc + v.y ); v.rotateCounterClockwise( arrowheadAngle ).setLength( r1 ); p.lineTo( xc + v.x, yc + v.y ); v.setLength( r2 ); p.lineTo( xc + v.x, yc + v.y ); p.arcTo( new RectF( xc - r2, yc - r2, xc + r2, yc + r2 ), -90 + deltaAInDeg - arrowheadAngle, -( deltaAInDeg - 2f * arrowheadAngle ) ); v.rotateCounterClockwise( F.abs( a1 - a2 ) - 2 * arrowheadAngle ).setLength( r1 ); p.lineTo( xc + v.x, yc + v.y ); p.close(); return p; 40 } Copyright (c) Richard Creamer 2011 - All Rights Reserved

Drawing Text

41

The FontMetrics Class and Paint.getTextBounds() • Android provides the android.graphics.Paint.FontMetrics class: • Provides Typeface dimensions useful for correctly positioning text • FontMetrics class attributes: • ascent - “Recommended” distance above baseline • bottom - Maximum distance below the base line for lowest glyph • descent - “Recommended” distance below baseline • leading - “Recommended” space between lines of text • top - Maximum distance above baseline for tallest glyph • Important: • Attribute values pertaining to distance above the baseline are NEGATIVE Most Useful Attributes

(-) top baseline

Glyph ( bottom - top )

bottom • Note: Paint.getTextBounds() often provides sufficient information ( FontMetrics object not always needed - see following examples... )

42 Copyright (c) Richard Creamer 2011 - All Rights Reserved

Drawing Text - Code General-purpose text drawing method supporting horizontal and vertical alignment public enum TextVertAlign { Top, Middle, Baseline, Bottom }; // Enumeration representing vertical alignment positions public static void drawHvAlignedText( Canvas canvas, float x, float y, String s, Paint p, Paint.Align horizAlign, TextVertAlign vertAlign ) { // Set horizontal alignment p.setTextAlign( horizAlign );

// Get bounding rectangle which we’ll need below... Rect r = new Rect(); p.getTextBounds( s, 0, s.length(), r ); // Note: r.top will be negative // Compute y-coordinate we'll need for drawing text for specified vertical alignment float textX = x; float textY = y; switch ( vertAlign ) { Also, look at: case Top: • Paint.measureText() textY = y - r.top; // Recall that r.top is negative • Paint.getFontMetrics() break; • android.text.StaticLayout case Middle: • android.widget.FrameLayout textY = y - r.top - r.height()/2; break; case Baseline: // Default behavior - no changes to y-coordinate break; case Bottom: textY = y - ( r.height() + r.top ); break; } canvas.drawText( s, textX, textY, p ); // Now we can draw the text with the proper ( x, y ) coordinates } 43 Copyright (c) Richard Creamer 2011 - All Rights Reserved

Text Drawing Example // Excerpts from TextDrawing demo... public enum TextVertAlign { Top, Middle, Baseline, Bottom }; private float fontScaleFactor = 0.08f; // Crude scaling to display private float crosshairScaleFactor = 0.05f; // Crude scaling to display private int numTextColumns = Paint.Align.values().length; private int numTextRows = TextVertAlign.values().length; private float crosshairSize = 10f; @Override protected void onDraw( Canvas canvas ) { canvas.drawPaint( bkgPaint ); // Clear background // Compute some variables we'll need float w = getWidth(); float h = getHeight(); crosshairSize = ( w < h ) ? w * crosshairScaleFactor : h * crosshairScaleFactor; float textSize = ( w < h ) ? w * fontScaleFactor : h * fontScaleFactor; textPaint.setTextSize( textSize ); float colWidth = ( w - 4 * crosshairSize ) / ( numTextColumns - 1 ); float lineSep = h / ( numTextRows + 1f ); float x = 2f * crosshairSize;

// Loop over horizontal and vertical alignment enum values // Draw string using loop values for horiz and vert alignment for ( Paint.Align ha : Paint.Align.values() ) { float y = lineSep; for ( TextVertAlign va : TextVertAlign.values() ) { drawHvAlignedText( canvas, x, y, s, textPaint, ha, va ); drawCrosshairs( canvas, x, y ); y += lineSep; } x += colWidth; }

Left

Center

Right

Top

Middle

Baseline

Bottom

} Copyright (c) Richard Creamer 2011 - All Rights Reserved

44

Optimization &Threading Considerations

45

Optimization • Because onDraw() is called frequently and because much of the Cool Clock graphics drawing is repetitive and computationally expensive, two techniques were used to reduce CPU usage and to maintain a responsive GUI: • Cache the screen bitmap ( background + clock face + ticks + numbers ) • The clock face only changes if: • The user modifies certain Preferences settings • The device orientation changes • As a result, the background and clock face are computed once into cached bitmaps, one for each orientation, until Preferences changes require updates. • Each time onDraw() is called, a cached bitmap is efficiently copied to the screen, then only the hands, circular arrows, highlights, and time text fields are drawn. • Compute the screen bitmap in a background thread: • To keep the GUI responsive, the long-running task of drawing the background and clock face are performed in a background thread - not in the GUI thread. • While a bitmap is not ready, onDraw() simply fills the screen with the background paint. • When the bitmap computation is completed and the background worker thread terminates, a new Runnable is posted to the GUI Handler which adopts the new Bitmap and then calls invalidate(). • invalidate() queues up a future call to onDraw() 46 Copyright (c) Richard Creamer 2011 - All Rights Reserved

Skeleton Definition of Background Worker Thread private static class ClockFaceRendererTask extends Thread { // Fields containing final defensive copies of a snapshot of ClockPanel fields required for rendering clock face bitmap ... private final Bitmap bm; // The output of this worker thread public ClockFaceRendererTask( final ClockPanel clockPanel ) { // Make final defensive copies of ClockPanel state data bm = rp.bm.getAndSet( null ); // Use AtomicReference to atomically assume exclusive ownership of Bitmap } public void run() { // Validate remaining input data renderClockFace(); // Thread terminates when run() completes } public Bitmap getFinalBitmap() { // Ensure renderingCompleted == true return bm; } private void renderClockFace() { // Draw background, clock face // Draw numbers, ticks renderingCompleted = true; } private void drawNumbers( Canvas canvas, float xc, float yc, float radius ) { ... } private void drawTicks( Canvas canvas, float xc, float yc, float radius ) { ... } }

47 Copyright (c) Richard Creamer 2011 - All Rights Reserved

Invoking the ClockFaceRendererTask • In order to create and run a clock renderer task in a background thread, we need to create an outer “monitor” thread to: • Instantiate and start a renderer task • Wait for the renderer sub-task to complete • After the renderer has finished computing a new bitmap, we need to define and post a Runnable task to the GUI handler which will run in the GUI thread and safely adopt the new Bitmap “later” • This is a bit complex for those new to threads ( consult the books listed on slide 11 for more details ) • Below is a diagram of what the next page’s callRenderer() method is doing... Non-GUI Worker Thread

1

callRenderer()

Defines complex thread

Instantiates ClockFaceRendererTask

Starts the ClockFaceRendererTask

Waits for the ClockFaceRendererTask to complete/terminate

Defines a Runnable which must be run in the GUI thread which adopts the newly computed Bitmap

2

callRenderer()

Starts outer thread and returns immediately

Posts the above Bitmap adoption Runnable to the GUI Handler which will run “later”

48 Copyright (c) Richard Creamer 2011 - All Rights Reserved

Invoking the ClockFaceRendererTask A worker thread launched and managed from within an outer monitor thread private void callRenderer() { // Called only in onDraw() - returns immediately after defining and starting background worker thread final ClockPanel cp = this; Thread t = new Thread( new Runnable() {

Define an outer thread to run in background which, in turn, starts and monitors a nested ClockFaceRendererTask thread

1

public void run() {

2

clockFaceRenderer = new ClockFaceRendererTask( cp ); // ClockFaceRendererTask extends Thread clockFaceRenderer.start(); // Start background renderer task asynchronously ( not in GUI thread ) // Wait for the renderer thread to terminate while ( true ) { try { clockFaceRenderer.join(); // join() waits for thread to terminate break; } catch ( InterruptedException ie ) { } }

Instantiate and start renderer task thread

Outer thread waits for renderer thread to complete

3

// New bitmap now computed - now have GUI thread adopt new bitmap reference and redraw itself final Bitmap newBitmap = clockFaceRenderer.getFinalBitmap(); if ( newBitmap == null ) throw new IllegalStateException( "Null bitmap returned from ClockFaceRendererTask!" ); guiThreadHandler.post( new Runnable() { public void run() { RenderParams rp = getCurRp(); Outer thread posts a sub-task to GUI Handler to rp.bm.set( newBitmap ); 4 adopt newly computed bitmap which will run computingBitmap = false; “at a later time” in the GUI thread invalidate(); } } );

} } ); // End outer thread definition // Start the thread we just defined - then return immediately t.start(); }

After defining the outer thread, we start it and return immediately

Copyright (c) Richard Creamer 2011 - All Rights Reserved

5

49

The Builder Pattern

50

Builder Pattern Use • The ClockPanel attributes can be initialized in many different ways depending on use context. • As a result, providing a Builder pattern to construct ClockPanel objects was implemented. • The main CoolClock Activity creates its ClockPanel component using this code: ClockState cs = PrefsActivity.getStoredPrefs( this ); // Get settings from previous Cool Clock execution clockPanel = new ClockPanel.ClockPanelBuilder(). // Construct a Builder object context( this ). // Set desired field values hideSecondHand( cs.hideSecondHand ). Note “dot” hideArcs( cs.hideCircularArrows ). operator use hideDigitalTime( cs.hideDigitalTime ). Reinstate the previous hideTextTime( cs.hidePhraseTime ). field values, then call clockOperationMode( cs.clockOperationMode ). buildClockPanel() to minuteNumDisplayMode( cs.minuteNumDisplayMode ). construct ClockPanel arcMode( cs.arcMode ). instance. buildClockPanel(); // Ask Builder to construct a ClockPanel object

Calls private ClockPanel constructor

51 Copyright (c) Richard Creamer 2011 - All Rights Reserved

The ClockPanel Constructor The only ClockPanel constructor accepts a ClockPanelBuilder argument: private ClockPanel( ClockPanelBuilder cpb ) { super( cpb.context ); // Call base class this.curPrefs = cpb.cs; // Data class encapsulating Preferences settings rpLand.orientation = E.Orientation.Landscape; // Data class encapsulating drawing geometry params rpPort.orientation = E.Orientation.Portrait; // Separate RenderParam object for both orientations guiThreadHandler = new Handler(); // Create Handler in the GUI thread curPrefs.enableHandDragging = ( curPrefs.clockOperationMode == E.ClockOperationMode.LearnMode ); setOnTouchListener( this ); // Subscribe to touch events }

52 Copyright (c) Richard Creamer 2011 - All Rights Reserved

Builder Pattern Implementation public static class ClockPanelBuilder { // Nested static class in ClockPanel.java

public ClockPanelBuilder hidePhraseTime( boolean hidePhraseTime ) { cs.hidePhraseTime = hidePhraseTime; return this; }

// Reference to CoolClock Activity ( needed by ClockPanel ) private Context context; // Start with default clock param values public ClockPrefs cs = ClockPrefs.defaultClockPrefs();

public ClockPanelBuilder hideCircularArrows( boolean hideCircularArrows ) { cs.hideCircularArrows = hideCircularArrows; return this; }

public ClockPanelBuilder() { } public ClockPanelBuilder context( Context context ) { this.context = context; return this; }

public ClockPanelBuilder minuteNumDisplayMode( E.MinuteNumDisplayMode minuteNumDisplayMode ) { cs.minuteNumDisplayMode = minuteNumDisplayMode; return this; }

public ClockPanelBuilder hideHourHand( boolean hideHourHand ) { cs.hideHourHand = hideHourHand; return this; }

public ClockPanelBuilder arcMode( E.ArcMode arcMode ) { cs.arcMode = arcMode; return this; }

public ClockPanelBuilder hideMinuteHand( boolean hideMinuteHand ) { cs.hideMinuteHand = hideMinuteHand; return this; }

public ClockPanelBuilder digitalTimeMode( E.DigitalTimeMode digitalTimeMode ) { cs.digitalTimeMode = digitalTimeMode; return this; }

public ClockPanelBuilder hideSecondHand( boolean hideSecondHand ) { cs.hideSecondHand = hideSecondHand; return this; }

public ClockPanelBuilder clockOperationMode( E.ClockOperationMode clockOperationMode) { cs.clockOperationMode = clockOperationMode; return this; }

public ClockPanelBuilder enableHandDragging( boolean enableHandDragging ) { cs.enableHandDragging = enableHandDragging; return this; }

public ClockPanel buildClockPanel() { if ( context == null ) throw new IllegalStateException( "Context field must be non-null before calling buildClockPanel()!" ); return new ClockPanel( this ); // Call private constructor }

public ClockPanelBuilder hideDigitalTime( boolean hideDigitalTime ) { cs.hideDigitalTime = hideDigitalTime; return this; } }

Copyright (c) Richard Creamer 2011 - All Rights Reserved

53

Cool Clock Helping children to read analog clocks

An introduction to developing custom 2D graphics Android applications Richard Creamer 2to32minus1 (at) gmail (dot) com

Links Home Page LinkedIn Profile Android Market Cool Clock Product Listing External Cool Apps Software Website External Cool Clock Product Details Page

54 Copyright (c) Richard Creamer 2011 - All Rights Reserved