[Android Software Development] michael.the.don:
[Ultimate Guide][Part 5-I-2/09/2011]Android App Development - From Scrap

PART V-I

Creating Compound Controls

Compound controls are atomic, reusable widgets that contain multiple child controls laid out and wired together. When you create a compound control, you define the layout, appearance, and interaction of the Views it contains. Compound controls are created by extending a ViewGroup (usually a Layout Manager). To create a new compound control, choose a layout class that’s most suitable for positioning the child controls, and extend it as shown in the skeleton code below:

public class MyCompoundView extends LinearLayout {

public MyCompoundView(Context context) {

super(context);

}

public MyCompoundView(Context context, AttributeSet attrs) {

super(context, attrs);

}

}

As with an Activity, the preferred way to design the UI for a compound control is to use a layout resource. The following code snippet shows the XML layout definition for a simple widget consisting of an Edit Text box and a button to clear it:

<?xml version=”1.0” encoding=”utf-8”?>

<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”

android:orientation=”vertical”

android:layout_width=”fill_parent”

android:layout_height=”fill_parent”>

<EditText

android:id=”@+id/editText”

android:layout_width=”fill_parent”

android:layout_height=”wrap_content”

/>

<Button

android:id=”@+id/clearButton”

android:layout_width=”fill_parent”

android:layout_height=”wrap_content”

android:text=”Clear”

/>

</LinearLayout>

To use this layout for your new widget, override its constructor to infl ate the layout resource using the inflate method from the LayoutInflate system service. The inflate method takes the layout resource and returns an infl ated View. For circumstances such as this where the returned View should be the class you’re creating, you can pass in a parent and attach the result to it automatically. The following code snippet shows the ClearableEditText class. Within the constructor it inflates the layout resource created above and gets references to each of the Views it contains. It also makes a call to hookupButton that will be used to hookup the clear text functionality when the button is pressed.

public class ClearableEditText extends LinearLayout {

EditText editText;

Button clearButton;

public ClearableEditText(Context context)

{

super(context);

String infService = Context.LAYOUT_INFLATER_SERVICE;

LayoutInflater li;

li = (LayoutInflater)getContext().getSystemService(infService);

li.inflate(R.layout.clearable_edit_text, this, true);

editText = (EditText)findViewById(R.id.editText);

clearButton = (Button)findViewById(R.id.clearButton);

// Hook up the functionality

hookupButton();

}

}

If you’d prefer to construct your layout in code, you can do so just as you would for an Activity. The following code snippet shows the ClearableEditText constructor overridden to create the same UI as is defined in the XML used in the earlier example:

public ClearableEditText(Context context) {

super(context);

// Set orientation of layout to vertical

setOrientation(LinearLayout.VERTICAL);

// Create the child controls.

editText = new EditText(getContext());

clearButton = new Button(getContext());

clearButton.setText(“Clear”);

// Lay them out in the compound control.

int lHeight = LayoutParams.WRAP_CONTENT;

int lWidth = LayoutParams.FILL_PARENT;

addView(editText, new LinearLayout.LayoutParams(lWidth, lHeight));

addView(clearButton, new LinearLayout.LayoutParams(lWidth, lHeight));

hookupButton();

}

Once the screen has been constructed, you can hook up the event handlers for each child control to provide the functionality you need. In this next snippet, the hookupButton method is fi lled in to clear the textbox when the button is pressed:

private void hookupButton() {

clearButton.setOnClickListener(new Button.OnClickListener() {

public void onClick(View v) {

editText.setText(“”);

}

});

}

Creating Custom Widgets and Controls

Creating completely new Views gives you the power to fundamentally shape the way your applications look and feel. By creating your own controls, you can create User Interfaces that are uniquely suited to your users’ needs. To create new controls from a blank canvas, you extend either the View or SurfaceView classes.

The View class provides a Canvas object and a series of draw methods and Paint classes, to create a visual interface using raster graphics. You can then override user events like screen touches or key presses to provide interactivity. In situations where extremely rapid repaints and 3D graphics aren’t required, the View base class offers a powerful lightweight solution.

The SurfaceView provides a canvas that supports drawing from a background thread and using openGL for 3D graphics. This is an excellent option for graphics-heavy controls that are frequently updated or display complex graphical information, particularly games and 3D visualizations.

Creating a New Visual Interface

The base View class presents a distinctly empty 100*100 pixel square. To change the size of the control and display a more compelling visual interface, you need to override the onMeasure and onDraw methods, respectively. Within onMeasure, the new View will calculate the height and width it will occupy given a set of boundary conditions. The onDraw method is where you draw on the Canvas to create the visual interface.

public class MyView extends View {

// Constructor required for in-code creation

public MyView(Context context) {

super(context);

}

// Constructor required for inflation from resource file

public MyView (Context context, AttributeSet ats, int defaultStyle) {

super(context, ats, defaultStyle );

}

//Constructor required for inflation from resource file

public MyView (Context context, AttributeSet attrs) {

super(context, attrs);

}

@Override

protected void onMeasure(int wMeasureSpec, int hMeasureSpec) {

int measuredHeight = measureHeight(hMeasureSpec);

int measuredWidth = measureWidth(wMeasureSpec);

// MUST make this call to setMeasuredDimension

// or you will cause a runtime exception when

// the control is laid out.

setMeasuredDimension(measuredHeight, measuredWidth);

}

private int measureHeight(int measureSpec) {

int specMode = MeasureSpec.getMode(measureSpec);

int specSize = MeasureSpec.getSize(measureSpec);

[ ... Calculate the view height ... ]

return specSize;

}

private int measureWidth(int measureSpec) {

int specMode = MeasureSpec.getMode(measureSpec);

int specSize = MeasureSpec.getSize(measureSpec);

[ ... Calculate the view width ... ]

return specSize;

}

@Override

protected void onDraw(Canvas canvas) {

[ ... Draw your visual interface ... ]

}

}

Drawing Your Control

The onDraw method is where the magic happens. If you’re creating a new widget from scratch, it’s because you want to create a completely new visual interface. The Canvas parameter available in the onDraw method is the surface you’ll use to bring your imagination to life. Android provides a variety of tools to help draw your design on the Canvas using various Paint objects.

The Canvas class includes helper methods to draw primitive 2D objects including circles, lines, rectangles, text, and Drawables (images). It also supports transformations that let you rotate, translate (move), and scale (resize) the Canvas while you draw on it.

Used in combination with Drawables and the Paint class (which offer a variety of customizable fills and pens), the complexity and detail that your control can render are limited only by the size of the screen and the power of the processor rendering it.

The following code snippet shows how to override the onDraw method to display a simple text string in the center of the control:

@Override

protected void onDraw(Canvas canvas) {

// Get the size of the control based on the last call to onMeasure.

int height = getMeasuredHeight();

int width = getMeasuredWidth();

// Find the center

int px = width/2;

int py = height/2;

// Create the new paint brushes.

// the widget’s constructor

Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

mTextPaint.setColor(Color.WHITE);

// Define the string.

String displayText = “Hello World!”;

// Measure the width of the text string.

float textWidth = mTextPaint.measureText(displayText);

// Draw the text string in the center of the control.

canvas.drawText(displayText, px-textWidth/2, py, mTextPaint);

}

Sizing Your Control

Unless you conveniently require a control that always occupies 100 100 pixels, you will also need to override onMeasure.

The onMeasure method is called when the control’s parent is laying out its child controls. It asks the question, “How much space will you use?” and passes in two parameters — widthMeasureSpec and heightMeasureSpec. They specify the space available for the control and some metadata describing that space. Rather than return a result, you pass the View’s height and width into the setMeasuredDimension method. The following skeleton code shows how to override onMeasure. Note the calls to local method stubs calculateHeight and calculateWidth. They’ll be used to decode the widthHeightSpec and heightMeasureSpec values and calculate the preferred height and width values.

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

int measuredHeight = measureHeight(heightMeasureSpec);

int measuredWidth = measureWidth(widthMeasureSpec);

setMeasuredDimension(measuredHeight, measuredWidth);

}

private int measureHeight(int measureSpec) {

// Return measured widget height.

}

private int measureWidth(int measureSpec) {

// Return measured widget width.

}

The boundary parameters, widthMeasureSpec and heightMeasureSpec, are passed in as integers for efficiency reasons. Before they can be used, they first need to be decoded using the static getMode and getSize methods from the MeasureSpec class, as shown in the snippet below:

int specMode = MeasureSpec.getMode(measureSpec);

int specSize = MeasureSpec.getSize(measureSpec);

Depending on the mode value, the size represents either the maximum space available for the control (in the case of AT_MOST), or the exact size that your control will occupy (for EXACTLY). In the case of UNSPECIFIED, your control does not have any reference for what the size represents. By marking a measurement size as EXACT, the parent is insisting that the View will be placed into an area of the exact size specifi ed. The AT_MOST designator says the parent is asking what size the View would like to occupy, given an upper bound. In many cases, the value you return will be the same. In either case, you should treat these limits as absolute. In some circumstances, it may still be appropriate to return a measurement outside these limits, in which case you can let the parent choose how to deal with the oversized View, using techniques such as clipping and scrolling. The following skeleton code shows a typical implementation for handling View measurement:

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

int measuredHeight = measureHeight(heightMeasureSpec);

int measuredWidth = measureWidth(widthMeasureSpec);

setMeasuredDimension(measuredHeight, measuredWidth);

}

private int measureHeight(int measureSpec) {

int specMode = MeasureSpec.getMode(measureSpec);

int specSize = MeasureSpec.getSize(measureSpec);

// Default size if no limits are specified.

int result = 500;

if (specMode == MeasureSpec.AT_MOST) {

// Calculate the ideal size of your

// control within this maximum size.

// If your control fills the available

// space return the outer bound.

result = specSize;

} else if (specMode == MeasureSpec.EXACTLY) {

// If your control can fit within these bounds return that value.

result = specSize;

}

return result;

}

private int measureWidth(int measureSpec) {

int specMode = MeasureSpec.getMode(measureSpec);

int specSize = MeasureSpec.getSize(measureSpec);

// Default size if no limits are specified.

int result = 500;

if (specMode == MeasureSpec.AT_MOST) {

// Calculate the ideal size of your control

// within this maximum size.

// If your control fills the available space

// return the outer bound.

result = specSize;

} else if (specMode == MeasureSpec.EXACTLY) {

// If your control can fit within these bounds return that value.

result = specSize;

}

return result;

}

Handling User Interaction Events

To make your new widget interactive, it will need to respond to user events like key presses, screen touches, and button clicks. Android exposes several virtual event handlers, listed below, that let you react to user input:

❑ onKeyDown Called when any device key is pressed; includes the D-pad, keyboard, hang-up, call, back, and camera buttons

❑ onKeyUp Called when a user releases a pressed key

❑ onTrackballEvent Called when the device’s trackball is moved

❑ onTouchEvent Called when the touch screen is pressed or released, or it detects movement

The following code snippet shows a skeleton class that overrides each of the user interaction handlers in a View:

@Override

public boolean onKeyDown(int keyCode, KeyEvent keyEvent) {

// Return true if the event was handled.

return true;

}

@Override

public boolean onKeyUp(int keyCode, KeyEvent keyEvent) {

// Return true if the event was handled.

return true;

}

@Override

public boolean onTrackballEvent(MotionEvent event ) {

// Get the type of action this event represents

int actionPerformed = event.getAction();

// Return true if the event was handled.

return true;

}

@Override

public boolean onTouchEvent(MotionEvent event) {

// Get the type of action this event represents

int actionPerformed = event.getAction();

// Return true if the event was handled.

return true;

}

Introducing the Android Menu System

To improve the usability of application menus, Android features a three-stage menu system optimized for small screens:

❑ The Icon Menu This compact menu appears along the bottom of the screen when the Menu button is pressed. It displays the icons and text for up to six Menu Items (or submenus). This icon menu does not display checkboxes, radio buttons, or the shortcut keys for Menu Items, so it’s generally good practice not to assign checkboxes or radio buttons to icon menu items, as

they will not be available. If more than six Menu Items have been defined, a More item is included that, when selected, displays

the expanded menu. Pressing the Back button closes the icon menu.

❑ The Expanded Menu The expanded menu is triggered when a user selects the More Menu Item from the icon menu. The expanded menu displays a scrollable list of only the Menu Items that weren’t visible in the icon menu. This menu displays full text, shortcut keys, and checkboxes/radio buttons as appropriate.

❑ Submenus The traditional “expanding hierarchical tree” can be awkward to navigate using a mouse, so it’s no surprise that this metaphor is particularly ill-suited for use on mobile devices. The Android alternative is to display each submenu in a floating window.

Defining an Activity Menu

To define a menu for an Activity, override its onCreateOptionsMenu method. This method is triggered the first time an Activity’s menu is displayed. The onCreateOptionsMenu receives a Menu object as a parameter. You can store a reference to, and continue to use, the Menu reference elsewhere in your code until the next time that onCreateOptionsMenu is called. You should always call through to the base implementation as it automatically includes additional system

menu options where appropriate.

Use the add method on the Menu object to populate your menu. For each Menu Item, you must specify:

❑ A group value to separate Menu Items for batch processing and ordering

❑ A unique identifi er for each Menu Item. For efficiency reasons, Menu Item selections are generally handled by the onOptionsItemSelected event handler, so this unique identifier is important to determine which Menu Item was pressed. It is convention to declare each menu ID as a private static variable within the Activity class. You can use the Menu.FIRST static constant and simply increment that value for each subsequent item.

❑ An order value that defines the order in which the Menu Items are displayed

❑ The menu text, either as a character string or as a string resource

When you have finished populating the menu, return True to allow Android to display the menu. The following skeleton code shows how to add a single item to an Activity menu:

static final private int MENU_ITEM = Menu.FIRST;

@Override

public boolean onCreateOptionsMenu(Menu menu) {

super.onCreateOptionsMenu(menu);

// Group ID

int groupId = 0;

// Unique menu item identifier. Used for event handling.

int menuItemId = MENU_ITEM;

// The order position of the item

int menuItemOrder = Menu.NONE;

// Text to be displayed for this menu item.

int menuItemText = R.string.menu_item;

// Create the menu item and keep a reference to it.

MenuItem menuItem = menu.add(groupId, menuItemId,

menuItemOrder, menuItemText);

return true;

}

Like the Menu object, each Menu Item reference returned by a call to add is valid until the next call to onCreateOptionsMenu. Rather than maintaining a reference to each item, you can find a particular. Menu Item by passing its ID into the Menu.findItem method.

Menu Item Options

Android supports most of the traditional Menu Item options you’re probably familiar with, including icons, shortcuts, checkboxes, and radio buttons, as described below:

❑ Checkboxes and Radio Buttons Checkboxes and radio buttons on Menu Items are visible in expanded menus and submenus. To set a Menu Item as a checkbox, use the setCheckable method. The state of that checkbox is controlled using setChecked. A radio button group is a group of items displaying circular buttons, where only one item can be selected at any given time. Checking one of these items will automatically unselect any other item in the same group, that is currently checked. To create a radio button group, assign the same group identifier to each item, then call Menu.setGroupCheckable, passing in that group identifier and setting the exclusive parameter to True. Checkboxes are not visible in the icon menu, so Menu Items that feature checkboxes should be reserved for submenus and items that appear only in the expanded menu. The following code snippet shows how to add a checkbox and a group of three radio buttons:

// Create a new check box item.

menu.add(0, CHECKBOX_ITEM, Menu.NONE, “CheckBox”).setCheckable(true);

// Create a radio button group.

menu.add(RB_GROUP, RADIOBUTTON_1, Menu.NONE, “Radiobutton 1”);

menu.add(RB_GROUP, RADIOBUTTON_2, Menu.NONE, “Radiobutton 2”);

menu.add(RB_GROUP, RADIOBUTTON_3, Menu.NONE,

“Radiobutton 3”).setChecked(true);

menu.setGroupCheckable(RB_GROUP, true, true);

❑ Shortcut Keys You can specify a keypad shortcut for a Menu Item using the setShortcut method. Each call to setShortcut requires two shortcut keys, one for use with the numeric keypad and a second to support a full keyboard. Neither key is case-sensitive. The code below shows how to set a shortcut for both modes:

menuItem.setShortcut(‘0’, ‘b’);

❑ Condensed Titles The icon menu does not display shortcuts or checkboxes, so it’s often necessary to modify its display text to indicate its state. The following code shows how to set the icon menu–specific text for a Menu Item:

menuItem.setTitleCondensed(“Short Title”);

❑ Icons Icon is a drawable resource identifier for an icon to be used in the Menu Item. Icons are only displayed in the icon menu; they are not visible in the extended menu or submenus. The following snippet shows how to apply an icon to a Menu Item:

menuItem.setIcon(R.drawable.menu_item_icon);

❑ Menu Item Click Listener An event handler that will execute when the Menu Item is selected. For efficiency reasons, this is discouraged; instead, Menu Item selections should be handled by the onOptionsItemSelected handler as shown later in this section. To apply a click listener to a Menu Item, use the pattern shown in the following code snippet:

menuItem.setOnMenuItemClickListener(new OnMenuItemClickListener() {

public boolean onMenuItemClick(MenuItem _menuItem) {

return true;

}

});