Android comes with many requirements that herald complexity in the user interface: it’s a multiprocessing system that supports multiple concurrent applications, accepts multiple forms of input, is highly interactive, and must be flexible enough to support a wide range of devices now and in the future. The user interface is impressively rich and easy to use, given all that it has to do. But you need to understand how it works in order to use it without crashing your application, making it look awful on some devices, or imposing a performance penalty on the system.
This chapter gives you basic techniques for writing a graphical interface on Android. It explains the architecture of the Android UI toolkit, while showing you in practical terms how to enable and lay out basic interface elements such as buttons and text boxes. It also covers event handling and other critical topics, such as using multiple threads to offload long tasks so that the UI doesn’t freeze.
The Android environment adds yet another Graphical User Interface (GUI) toolkit to the Java ecosphere, joining AWT, Swing, SWT, and J2ME (leaving aside the web UI toolkits). If you’ve worked with any of these, the Android framework will look familiar. Like them, it is single-threaded, event-driven, and built on a library of nestable components.
The Android UI framework is, like the other UI frameworks, organized around the common Model-View-Controller pattern illustrated in Figure 10-1. It provides structure and tools for building a Controller that handles user input (like key presses and screen taps) and a View that renders graphical information to the screen.
The Model is the guts of your application: what it actually does. It might be, for instance, the database of tunes on your device and the code for playing them. It might be your list of contacts and the code that places phone calls or sends IMs to them.
While a particular application’s View and Controller will necessarily reflect the Model they manipulate, a single Model might be used by several different applications. Think, for instance, of an MP3 player and an application that converts MP3 files into WAV files. For both applications, the Model includes the MP3 file format and codecs for it. The former application, however, has the familiar Stop, Start, and Pause controls, and plays the track. The latter may not produce any sound at all; instead, it will have controls for setting bitrate, etc. The Model is all about the data. It is the subject of most of the rest of this book.
The View is the application’s feedback to the user. It is the
portion of the application responsible for rendering the display,
sending audio to speakers, generating tactile feedback, and so on.
The graphical portion of the Android UI framework’s View, described
in detail in Chapter 12, is
implemented as a tree of subclasses of the View
class. Graphically, each of these objects represents a rectangular area
on the screen that is completely within the rectangular area
represented by its parent in the tree. The root of this tree is the
application window.
As an example, the display in a hypothetical MP3 player might contain a component that shows the album cover for the currently playing tune. Another component might display the name of the currently playing song, while a third contains subcomponents such as the Play, Pause, and Stop buttons.
The UI framework paints the screen by walking the View tree, asking each component to draw itself in a preorder traversal. In other words, each component draws itself and then asks each of its children to do the same. When the whole tree has been rendered, the smaller, nested components that are the leaves of the tree—and that were, therefore, painted later—appear to be painted on top of the components that are nearer to the root and that were painted first.
The Android UI framework is actually quite a bit more efficient than this oversimplified description suggests. It does not paint an area of a parent view if it can be certain that some child will later paint the same area, because it would be a waste of time to paint background underneath an opaque object! It would also be a waste of time to repaint portions of a view that have not changed.
The Controller is the portion of an application that responds to external actions: a keystroke, a screen tap, an incoming call, etc. It is implemented as an event queue. Each external action is represented as a unique event in the queue. The framework removes each event from the queue in order and dispatches it.
For example, when a user presses a key on his phone, the
Android system generates a KeyEvent
and adds it to an event queue. Eventually,
after previously enqueued events have been processed, the KeyEvent
is removed from the queue and
passed as the parameter of a call to the dispatchKeyEvent
method of the View
that
is currently selected.
Once an event is dispatched to the in-focus component, that component may take appropriate action to change the internal state of the program. In an MP3 player application, for instance, when the user taps a Play/Pause button on the screen and the event is dispatched to that button’s object, the handler method might update the Model to resume playing some previously selected tune.
This chapter describes the construction of a Controller for an Android application.
We now have all the concepts necessary to describe the complete UI system. When an external action occurs (for example, when the user scrolls, drags, or presses a button; a call comes in; or an MP3 player arrives at the end of its playlist), the Android system enqueues an event representing the action on the event queue. Eventually the event is dequeued—first in, first out—and dispatched to an appropriate event handler. The handler, probably code you write as part of your application, responds to the event by notifying the Model that there has been a change in state. The Model takes the appropriate action.
Nearly any change in Model state will require a corresponding change in the View. In
response to a key press, for instance, an EditText
component must show the newly
typed character at the insertion point. Similarly, in a phone book
application, clicking on a contact will cause that contact to be
highlighted and the previously highlighted contact to have its
highlighting removed.
In order to update the display, the Model must notify the UI Framework that some portion of the display is now stale and has to be redrawn. The redraw request is, actually, nothing more than another event enqueued in the same framework event queue that held the Controller event a moment ago. The redraw event is processed, in order, like any other UI event.
Eventually, the redraw event is removed from the queue and
dispatched. The event handler for a redraw event is the View. The
View
tree is redrawn, and each
View
object is responsible for
rendering its current state at the time it is drawn.
To make this concrete, we can trace the cycle through a hypothetical MP3 player application:
When the user taps the screen image of the Play/Pause
button, the framework creates a new MotionEvent
containing, among other
things, the screen coordinates of the tap. The framework
enqueues the new event at the end of the event queue.
As described in The Controller, when the event percolates through the queue, the framework removes it and passes it down the View tree to the leaf widget within whose bounding rectangle the tap occurred.
Because the button widget represents the Play/Pause button, the application button handling code tells the core (the Model) that it should resume playing a tune.
The application Model code starts playing the selected tune. In addition, it sends a redraw request to the UI framework.
The redraw request is added to the event queue and eventually processed as described in The View.
The screen gets redrawn with the Play button in its playing state, and everything is again in sync.
UI component objects such as buttons and text boxes actually
implement both View and Controller methods. This only makes sense.
When you add a Button
to your
application’s UI, you want it to appear on the screen as well as do
something when the user pushes it. Even though the two logical
elements of the UI—the View and the Controller—are implemented in
the same object, you should take care that they do not directly
interact. Controller methods, for instance, should never directly
change the display. Leave it to the code that actually changes state
to request a redraw, and trust that later calls to rendering methods
will allow the component to reflect its new state. Coding in this
way minimizes synchronization problems and helps to keep your
program robust and bug-free.
There is one more aspect of the Android UI framework that it is important to understand: it is single-threaded. There is a single thread removing events from the event queue to make Controller callbacks and to render the View. This is significant for several reasons.
The simplest consequence of a single-threaded UI is that it is
not necessary to use synchronized
blocks to coordinate state between the View and the
Controller. This is a valuable optimization.
Another advantage of a single-threaded UI is the guarantee that each event on the event queue is processed completely and in the order in which it was enqueued. That may seem fairly obvious, but its implications make coding the UI much easier. When a UI component is called to handle an event, it is guaranteed that no additional UI processing will take place until it returns. That means, for instance, that when a component requests multiple changes in the program state—each of which causes a corresponding request that the screen be repainted—it is guaranteed that the repaint will not start until it has completed processing, performed all of its updates, and returned. In short, UI callbacks are atomic.
There is a third reason to remember that there is only a single thread dequeuing and dispatching events from the UI event queue: if your code stalls that thread, for any reason, your UI will freeze! If a component’s response to an event is simple (changing the state of variables, creating new objects, etc.), it is perfectly correct to do that processing on the main event thread. If, on the other hand, the handler must retrieve a response from some distant network service or run a complex database query, the entire UI will become unresponsive until the request completes. That definitely does not make for a great user experience! Long-running tasks must be delegated to another thread, as described in Advanced Wiring: Focus and Threading.
The Android UI framework provides both a complete set of
drawing tools with which to build a UI and a rich collection of
prebuilt components based on these tools. As we will see in Chapter 12, the framework graphics tools
provide plenty of support for applications that need to create their
own controls or render special views. On the other hand, many
applications may work very well using only canned widgets from the
toolkit. In fact, as we saw in Chapter 9, the MapActivity
and MyLocationOverlay
classes make it possible
to create extremely sophisticated applications without doing any
custom drawing at all.
We’ve already used the term “widget” once or twice, without
explicitly defining it. Recall that the screen is a rendered by a tree
of components. In the Android UI framework, these components are all
subclasses of android.view.View
.
The components that are leaves or nearly leaves do most of the actual
drawing and are, in the context of an application UI, commonly called
widgets.
The internal nodes, sometimes called Container
Views, are special components that can have other components as
children. In the Android UI framework, Container Views are subclasses
of android.view.ViewGroup
,
which, of course, is in turn a subclass of View
. Typically, they do very little
drawing. Instead, they are responsible for arranging their child
components on the screen and keeping them arranged as the display
changes shape, orientation, and so on. Doing this can be quite
complex.
You have already seen a very simple View
coded up in Writing HelloWorld. That application created a
trivial TextView
and displayed it.
There is no way to add anything to that application, because the root
View
is a TextView
, which cannot be a container for
other components. To create more complex displays, it is necessary to
assemble a tree of containers. Example 10-1 shows an application with a
view tree that is three layers deep. A vertical linear layout contains
two horizontal linear layouts. Each of the horizontal layouts, in
turn, contains two widgets.
package com.oreilly.android.intro; import android.app.Activity; import android.graphics.Color; import android.os.Bundle; import android.view.Gravity; import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; public class AndroidDemo extends Activity { private LinearLayout root; @Override public void onCreate(Bundle state) { super.onCreate(state); LinearLayout.LayoutParams containerParams = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0.0F); LinearLayout.LayoutParams widgetParams = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT, 1.0F); root = new LinearLayout(this); root.setOrientation(LinearLayout.VERTICAL); root.setBackgroundColor(Color.LTGRAY); root.setLayoutParams(containerParams); LinearLayout ll = new LinearLayout(this); ll.setOrientation(LinearLayout.HORIZONTAL); ll.setBackgroundColor(Color.GRAY); ll.setLayoutParams(containerParams); root.addView(ll); EditText tb = new EditText(this); tb.setText(R.string.defaultLeftText); tb.setFocusable(false); tb.setLayoutParams(widgetParams); ll.addView(tb); tb = new EditText(this); tb.setText(R.string.defaultRightText); tb.setFocusable(false); tb.setLayoutParams(widgetParams); ll.addView(tb); ll = new LinearLayout(this); ll.setOrientation(LinearLayout.HORIZONTAL); ll.setBackgroundColor(Color.DKGRAY); ll.setLayoutParams(containerParams); root.addView(ll); Button b = new Button(this); b.setText(R.string.labelRed); b.setTextColor(Color.RED); b.setLayoutParams(widgetParams); ll.addView(b); b = new Button(this); b.setText(R.string.labelGreen); b.setTextColor(Color.GREEN); b.setLayoutParams(widgetParams); ll.addView(b); setContentView(root); } }
Note that the code preserves a reference to the root of the View tree for later use.
This example uses three LinearLayout
widgets. A LinearLayout
, just
as its name implies, is a View
that
displays its children in a row or column, as determined by its
orientation property. The child views are displayed in the order in
which they are added to the LinearLayout
(regardless of the order in
which they were created), in the directions
familiar to Western readers: left to right and top to bottom. The
button labeled “Green”, for instance, is in the lower righthand corner
of this layout, because it is the second thing added to the horizontal
LinearLayout
View, which was, in
turn, the second thing added to the vertical LinearLayout
(the root).
Figure 10-2 shows what the
results might look like to the user. The seven View
s in the tree are structured as shown in
Figure 10-3.
Chapter 8 explained that the Android framework provides a convenient capability for separating data resources from code. This is particularly useful in building view component layouts. The previous example can be replaced with the dramatically simpler code in Example 10-2 and the XML definition of the layout in Example 10-3.
package com.oreilly.android.intro; import android.app.Activity; import android.os.Bundle; /** * Android UI demo program */ public class AndroidDemo extends Activity { private LinearLayout root; @Override public void onCreate(Bundle state) { super.onCreate(state); setContentView(R.layout.main); root = (LinearLayout) findViewById(R.id.root); } }
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/root" android:orientation="vertical" android:background="@drawable/lt_gray" android:layout_width="fill_parent" android:layout_height="wrap_content"> <LinearLayout android:orientation="horizontal" android:background="@drawable/gray" android:layout_width="fill_parent" android:layout_height="wrap_content"> <EditText android:id="@+id/text1" android:text="@string/defaultLeftText" android:focusable="false" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1"/> <EditText android:id="@+id/text2" android:text="@string/defaultRightText" android:focusable="false" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1"/> </LinearLayout> <LinearLayout android:orientation="horizontal" android:background="@drawable/dk_gray" android:layout_width="fill_parent" android:layout_height="wrap_content"> <Button android:id="@+id/button1" android:text="@string/labelRed" android:textColor="@drawable/red" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1"/> <Button android:id="@+id/button2" android:text="@string/labelGreen" android:textColor="@drawable/green" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1"/> </LinearLayout> </LinearLayout>
This version of the code, like the first one, also preserves a
reference to the root of the View tree. It does this by tagging a
widget in the XML layout (the root LinearLayout
, in this case) with an android:id
tag, and then using the findViewById
method from the Activity
class to recover the reference.
It is a very good idea to get into the habit of using a resource to define your View tree layout. Doing so allows you to separate the visual layout from the code that brings it to life. This way, you can tinker with the layout of a screen without recompiling. Further, if the history of other UI frameworks is any indication, there will eventually be tools for Android that allow you to compose screens, creating their XML definitions, using a visual UI editor.
Assembling a Graphical Interface demonstrated a view with two buttons. Although the buttons look nice—they even highlight when clicked—they aren’t very useful. Clicking them doesn’t actually do anything.
The discussion of The Controller described how the Android framework translates external actions (screen taps, key presses, etc.) into events that are enqueued and then passed into the application. Example 10-4 shows how to add an event handler to one of the buttons in the demo, so that it does something when it is clicked.
@Override public void onCreate(Bundle state) { super.onCreate(state); setContentView(R.layout.main); final EditText tb1 = (EditText) findViewById(R.id.text1); final EditText tb2 = (EditText) findViewById(R.id.text2); ((Button) findViewById(R.id.button2)).setOnClickListener( new Button.OnClickListener() { @Override public void onClick(View arg0) { tb1.setText(String.valueOf(rand.nextInt(200))); tb2.setText(String.valueOf(rand.nextInt(200))); } } ); }
When run, this version of the application still looks a lot like
Figure 10-2. Unlike the earlier
example, though, in this version every time a user clicks the button
labeled “Green”, the numbers in the EditText
boxes change. This is illustrated
in Figure 10-4.
Simply changing numbers isn’t very interesting, but this small
example demonstrates the standard mechanism that an application uses
to respond to UI events. It is important to note that, appearances
notwithstanding, this example does not violate the MVC separation of
concerns. In response to the call to setText
, in this implementation of an
OnClickListener
, the EditText
object updates an internal
representation of the text it should display, and then calls its own
invalidate
method. It does not
immediately draw on the screen. There are very few rules in
programming that are absolute, but the admonition to separate the
Model, the View, and the Controller comes pretty close.
In the example, the instance of the Button
class is wired to its behavior using its setOnClickListener
method. Button
is a subclass of View
, which defines an interface named
OnClickListener
and a method named
setOnClickListener
, which registers
the listener. The OnClickListener
interface defines a single method, onClick
. When a Button
receives an event from the framework, in addition to any
other processing it might do, it examines the event to see whether it
qualifies as a “click.” (The button in Example 10-1 would highlight when
pressed, even before the listener was added.) If the event does
qualify as a click and if a click listener has been installed, that
listener’s onClick
method is
invoked.
The click listener is free to implement any custom behavior
that’s needed. In the example, the custom behavior creates two random
numbers between 0 and 200 and puts one into each of the two text
boxes. Instead of subclassing Button
and overriding its event processing
methods, all that is necessary to extend its behavior is to register a
click listener that implements the behavior. Certainly a lot
easier!
The click handler is especially interesting because at the heart
of the Android system—the
framework event queue—there is no such thing as a click event.
Instead, View
event processing
synthesizes the concept of a “click” from other events. If the device
has a touch-sensitive screen, for instance, a single tap is considered
a click. If the device has a center key in its D-pad or an “Enter”
key, pressing and releasing either will also register as a click.
View
clients need not concern
themselves with what a click is or how it is generated on a particular
device. They need only handle the higher-level concept, leaving the
details to the framework.
A View
can have only one
OnClickListener
. Calling setOnClickListener
a second time on a given
View
will remove the old listener
and install the new one. On the other hand, a single listener can
listen to more than one View
. The
code in Example 10-5, for
instance, is part of another application that looks exactly like Example 10-2. In this version, though, pushing
either of the buttons will update the text
box.
This capability can be very convenient in an application in which several actions produce the same behavior. Do not be tempted, though, to create a single enormous listener to handle all your widgets. Your code will be easier to maintain and modify if it contains multiple smaller listeners, each of which implements a single, clear behavior.
@Override public void onCreate(Bundle state) { super.onCreate(state); setContentView(R.layout.main); final EditText tb1 = (EditText) findViewById(R.id.text1); final EditText tb2 = (EditText) findViewById(R.id.text2); Button.OnClickListener listener = new Button.OnClickListener() { @Override public void onClick(View arg0) { tb1.setText(String.valueOf(rand.nextInt(200))); tb2.setText(String.valueOf(rand.nextInt(200))); } }; ((Button) findViewById(R.id.button1)).setOnClickListener(listener); ((Button) findViewById(R.id.button2)).setOnClickListener(listener); }
The Android UI framework uses the handler installation pattern
pervasively. Although our earlier examples were all Button
s, many other Android widgets define
listeners. The View
class defines
several events and listeners that are ubiquitous, and which we will
explore in further detail later in this chapter. Other classes,
however, define other specialized types of events and provide
handlers for those events that are meaningful only for those
classes. This is a standard idiom that allows clients to customize
the behavior of a widget without having to subclass it.
This pattern (called the Callback Pattern) is also an excellent way for your program to handle its own external, asynchronous actions. Whether responding to a change in state on a remote server or an update from a location-based service, your application can define its own events and listeners to allow its clients to react.
The examples so far have been elementary and have cut several
corners. Although they demonstrate connecting a View and a
Controller, they have not had real Models. (Example 10-4 actually used a String
owned by the implementation of
EditText
as a Model.) In order to
proceed, we’re going to have to take a brief detour to build a real,
usable Model.
The two classes in Example 10-6 comprise a Model that will support extensions to the demo application for this chapter. They provide a facility for storing a list of objects, each of which has X and Y coordinates, a color, and a size. They also provide a way to register a listener, and an interface that the listener must implement.
package com.oreilly.android.intro.model; /** A dot: the coordinates, color and size. */ public final class Dot { private final float x, y; private final int color; private final int diameter; /** * @param x horizontal coordinate. * @param y vertical coordinate. * @param color the color. * @param diameter dot diameter. */ public Dot(float x, float y, int color, int diameter) { this.x = x; this.y = y; this.color = color; this.diameter = diameter; } /** @return the horizontal coordinate. */ public float getX() { return x; } /** @return the vertical coordinate. */ public float getY() { return y; } /** @return the color. */ public int getColor() { return color; } /** @return the dot diameter. */ public int getDiameter() { return diameter; } } package com.oreilly.android.intro.model; import java.util.Collections; import java.util.LinkedList; import java.util.List; /** A list of dots. */ public class Dots { /** DotChangeListener. */ public interface DotsChangeListener { /** @param dots the dots that changed. */ void onDotsChange(Dots dots); } private final LinkedList<Dot> dots = new LinkedList<Dot>(); private final List<Dot> safeDots = Collections.unmodifiableList(dots); private DotsChangeListener dotsChangeListener; /** @param l the new change listener. */ public void setDotsChangeListener(DotsChangeListener l) { dotsChangeListener = l; } /** @return the most recently added dot, or null. */ public Dot getLastDot() { return (dots.size() <= 0) ? null : dots.getLast(); } /** @return the list of dots. */ public List<Dot> getDots() { return safeDots; } /** * @param x dot horizontal coordinate. * @param y dot vertical coordinate. * @param color dot color. * @param diameter dot size. */ public void addDot(float x, float y, int color, int diameter) { dots.add(new Dot(x, y, color, diameter)); notifyListener(); } /** Delete all the dots. */ public void clearDots() { dots.clear(); notifyListener(); } private void notifyListener() { if (null != dotsChangeListener) { dotsChangeListener.onDotsChange(this); } } }
In addition to using this model, the next example also
introduces a widget used to view it, the DotView
: it will be discussed later, in
Example 12-3. For now we
introduce it as a library widget. Its job is to draw the dots
represented in the Model, in the correct color and at the correct
coordinates. The complete source for the application is on the
website for this book.
Example 10-7 shows the new demo application, after adding the new Model and View.
package com.oreilly.android.intro; import java.util.Random; import android.app.Activity; import android.graphics.Color; import android.os.Bundle; import android.view.View; import android.widget.Button; 10 import android.widget.EditText; import android.widget.LinearLayout; import com.oreilly.android.intro.model.Dot; import com.oreilly.android.intro.model.Dots; import com.oreilly.android.intro.view.DotView; /** Android UI demo program */ public class TouchMe extends Activity { public static final int DOT_DIAMETER = 6; private final Random rand = new Random(); final Dots dotModel = new Dots(); DotView dotView; /** Called when the activity is first created. */ @Override public void onCreate(Bundle state) { super.onCreate(state); dotView = new DotView(this, dotModel); // install the view setContentView(R.layout.main); ((LinearLayout) findViewById(R.id.root)).addView(dotView, 0); // wire up the controller ((Button) findViewById(R.id.button1)).setOnClickListener( new Button.OnClickListener() { @Override public void onClick(View v) { makeDot(dots, dotView, Color.RED); } }); ((Button) findViewById(R.id.button2)).setOnClickListener( new Button.OnClickListener() { @Override public void onClick(View v) { makeDot(dots, dotView, Color.GREEN); } }); final EditText tb1 = (EditText) findViewById(R.id.text1); final EditText tb2 = (EditText) findViewById(R.id.text2); dots.setDotsChangeListener(new Dots.DotsChangeListener() { @Override public void onDotsChange(Dots d) { Dot d = dots.getLastDot(); tb1.setText((null == d) ? "" : String.valueOf(d.getX())); tb2.setText((null == d) ? "" : String.valueOf(d.getY())); dotView.invalidate(); } }); } /** * @param dots the dots we're drawing * @param view the view in which we're drawing dots * @param color the color of the dot */ void makeDot(Dots dots, DotView view, int color) { int pad = (DOT_DIAMETER + 2) * 2; dots.addDot( DOT_DIAMETER + (rand.nextFloat() * (view.getWidth() - pad)), DOT_DIAMETER + (rand.nextFloat() * (view.getHeight() - pad)), color, DOT_DIAMETER); } }
Here are some of the highlights of the code:
These two calls to setOnClickListener
add new listeners
to the layout obtained from the XML definition.
Anonymous classes handle click event callbacks to the
“Red” and “Green” buttons. These event handlers differ from
those in the previous example only in that here their behavior
has been factored out into the local method makeDot
, described in item 5.
Calls to makeDot
within
onClick
(to take place when a
button is clicked).
The most substantial change to the example. This is where
the Model is wired to the View, using the Callback pattern, by
installing a dotsChangedListener
. When the Model
changes, this new listener is called. It installs the X and Y
coordinates of the last dot into the left and right text boxes,
respectively, and requests that the DotView
redraw itself (the invalidate
call).
Definition of makeDot
.
This new method creates a dot, checking to make sure it is
within the DotView
’s borders,
and adds it to the Model. It also allows the dot’s color to be
specified as a parameter.
Figure 10-5 shows what the application looks like when run.
Pushing the button labeled “Red” adds a new red dot to the
DotView
. Pushing the “Green”
button adds a green one. The text fields contain the coordinates of
the last dot added.
The basic structure of Example 10-2 is still recognizable, with some extensions. For example, here is the chain of events that results from clicking the “Green” button:
When the button is clicked, its clickHandler
is called.
This causes a call to the anonymous class installed as an
OnClickHandler
. It, in turn,
calls makeDot
with the color
argument Color.GREEN
. The
makeDot
method generates
random coordinates and adds a new green Dot
to the Model at those
coordinates.
When the Model is updated, it calls its DotsChangedListener
.
The listener updates the values in the text views and
requests that the DotView
be
redrawn.
Modifying the demo application to
handle taps is just a matter of adding a tap handler. The code in
Example 10-8 extends the
demo application to place a cyan dot in the DotView
at the point at which the screen
is tapped. In the previous example, the code would be added at the
beginning of the onCreate
function right after the call to its parent method. Notice that,
because the code that displays the X and Y coordinates of the most
recently added dot is wired only to the Model, it continues to work
correctly, no matter how dots are added.
dotView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (MotionEvent.ACTION_DOWN != event.getAction()) { return false; } dots.addDot(event.getX(), event.getY(), Color.CYAN, DOT_DIAMETER); return true; } });
The MotionEvent
passed to
the handler has several other properties in addition to the location
of the tap that caused it. As the example indicates, it also
contains the event type, one of DOWN
, UP
, MOVE
, or CANCEL
. A simple tap actually generates
one DOWN
and one UP
event. Touching and dragging generates
a DOWN
event, a series of
MOVE
events, and a final UP
event.
The facilities provided by the MotionEvent
for handling gestures are very
interesting. The event contains the size of the touched area and the
amount of pressure applied. That means that, on devices that support
it, an application might be able to distinguish between a tap with
one finger and a tap with two fingers, or between a very light brush
and a firm push.
Efficiency is still important in the mobile world. A UI
framework confronts the horns of a dilemma when tracking and
reporting touchscreen events. Reporting too few events might make it
impossible to follow motion with sufficient accuracy to do, for
instance, handwriting recognition. On the other hand, reporting too
many touch samples, each in its own event, could load the system
unacceptably. The Android UI framework addresses this problem by
bundling groups of samples together, reducing the load and still
maintaining accuracy. To see all of the samples associated with an
event, use the history facility implemented with the methods
getHistoricalX
, getHistoricalY
, etc.
Example 10-9 shows how to
use the history facility. It extends the demo program to track a
user’s gestures when she touches the screen. The framework delivers
sampled X and Y coordinates to
the onTouch
method of an object
installed as the OnTouchListener
for the DotView
. The method displays a cyan dot
for each sample.
private static final class TrackingTouchListener implements View.OnTouchListener { private final Dots mDots; TrackingTouchListener(Dots dots) { mDots = dots; } @Override public boolean onTouch(View v, MotionEvent evt) { switch (evt.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: for (int i = 0, n = evt.getHistorySize(); i < n; i++) { addDot( mDots, evt.getHistoricalX(i), evt.getHistoricalY(i), evt.getHistoricalPressure(i), evt.getHistoricalSize(i)); } break; default: return false; } addDot( mDots, evt.getX(), evt.getY(), evt.getPressure(), evt.getSize()); return true; } private void addDot(Dots dots, float x, float y, float p, float s) { dots.addDot( x, y, Color.CYAN, (int) ((p * s * Dot.DIAMETER) + 1)); } }
Here are some highlights of the code:
This loop handles batched historical events. When touch
samples change more quickly than the framework can deliver
them, it bundles them into a single event. The MotionEvent
method getHistorySize
returns the number of
samples in the batch, and the various getHistory
methods get the subevent
specifics.
Figure 10-6 shows what the extended version of the application might look like after a few clicks and drags.
The implementation uses the size and pressure at a given location’s sample to determine the diameter of the dot drawn there. Unfortunately, the Android emulator does not emulate touch pressure and size, so all of the dots have the same diameter. Size and pressure values are normalized across devices, as floating-point values between 0.0 and 1.0, depending on the calibration of the screen. It is possible, however, that either value may actually be larger than 1.0. At the other end of the range, the emulator always reports the event size as zero.
Devices with trackballs also generate MotionEvents
when the trackball is moved.
These events are similar to those generated by taps on a
touch-sensitive screen, but they are handled differently. Trackball
MotionEvents
are passed into the
View
through a call to dispatchTrackballEvent
, not to dispatchTouchEvent
, which delivered taps.
Although dispatchTrackballEvent
does pass the event to onTrackballEvent
, it does not first pass
the event to a listener! Not only are trackball-generated MotionEvents
not visible through the
normal tap-handling machinery, but, in order to respond to them, a
widget must subclass View
and
override the onTrackballEvent
method.
MotionEvents
generated by
the trackball are handled differently in yet another way. If they
are not consumed (to be defined shortly) they
are converted into D-pad key events (like those that would be
generated by left, right, up and down arrow keys). This makes sense
when you consider that most devices have either a D-pad or a
trackball, but not both. Without this conversion, it wouldn’t be
possible to generate D-pad events on a device with only a trackball.
Of course, it also implies that an application that handles
trackball events must do so carefully, lest it break the
translation.
Handling keystroke input across multiple platforms can be very
tricky. Some devices have many more keys than others, some require
triple-tapping for character input, and so on. This is a great
example of something that should be left to the framework (EditText
or one of its subclasses)
whenever possible.
To extend a widget’s KeyEvent
handling, use the View
method setOnKeyListener
to install an OnKeyListener
. The listener will be called
with multiple KeyEvent
s for each
user keystroke, one for each action type: DOWN
, UP
, and MULTIPLE
. The action types DOWN
and UP
indicate a key was pressed or released,
just as they did for the MotionEvent
class. A key action of
MULTIPLE
indicates that a key is
being held down (autorepeating). The KeyEvent
method getRepeatCount
gives the number of
keystrokes that a MULTIPLE
event
represents.
Example 10-10 shows a sample key handler. When added to the demo program, it causes dots to be added to the display at randomly chosen coordinates when keys are pressed and released: a magenta dot when the Space key is pressed and released, a yellow dot when the Enter key is pressed and released, and a blue dot when any other key is pressed and released.
dotView.setFocusable(true); dotView.setOnKeyListener(new OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (KeyEvent.ACTION_UP != event.getAction()) { int color = Color.BLUE; switch (keyCode) { case KeyEvent.KEYCODE_SPACE: color = Color.MAGENTA; break; case KeyEvent.KEYCODE_ENTER: color = Color.YELLOW; break; default: ; } makeDot(dots, dotView, color); } return true; } });
You’ve probably noticed that the on...
methods of all of the listeners introduced thus far—including
onKey
—return a boolean
value. This is a pattern for
listeners that allows them to control subsequent event processing by
their caller.
When a Controller event is handed to a widget, the framework
code in the widget dispatches
it to an appropriate method, depending on its type: onKeyDown
, onTouchEvent
, etc. These
methods, either in View
or
one its subclasses, implement the widget’s behavior. As described
earlier, though, the framework first offers the event to an
appropriate listener (OnTouchListener
, OnKeyListener
, etc.) if one exists. The
listener’s return value determines whether the event is then
dispatched to the View
methods.
If the listener returns false
, the event is dispatched to the
View
methods as if the handler
did not exist. If, on the other hand, a listener returns true
, the event is said to have been
consumed. The View
aborts any
further processing for it. The View
methods are never called and have no
opportunity to process or respond to the event. As far as the
View
methods are concerned, it is
as if the event did not exist.
There are three ways that an event might be processed:
The event is dispatched to the View
methods for normal handling. A
widget implementation may, of course, override these
methods.
Listener event handling completely replaces normal
widget event handling. The event is never dispatched to the
View
.
The event is processed by the listener and then by the
View
. After listener event
handling is completed, the event is dispatched to the View
for normal handling.
Consider, for instance, what would happen if the key listener
from Example 10-10 were added to an
EditText
widget. Since the
onKey
method always returns
true
, the framework will abort
any further KeyEvent
processing
as soon as the method returns. That would prevent the EditText
key-handling mechanism from ever
seeing the key events, and no text would ever appear in the text
box. That is probably not the intended behavior!
If the onKey
method instead returns false
for some key events, the framework
will dispatch those events to the EditText
widget for continued processing.
The EditText
mechanism will see
the events, and the associated characters will be appended to the
EditText
box, as expected. Example 10-11 shows an extension of Example 10-10 that, besides adding new dots
to the Model, also filters the characters passed to the hypothetical
EditText
box. It allows numeric
characters to be processed normally but hides everything
else.
new OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (KeyEvent.ACTION_UP != event.getAction()) { int color = Color.BLUE; switch (keyCode) { case KeyEvent.KEYCODE_SPACE: color = Color.MAGENTA; break; case KeyEvent.KEYCODE_ENTER: color = Color.YELLOW; break; default: ; } makeDot(dotModel, dotView, color); } return (keyCode < KeyEvent.KEYCODE_0) ||(keyCode > KeyEvent.KEYCODE_9); } }
If your application needs to implement entirely new ways of
handling events (in other words, if it is something that cannot be
implemented reasonably by augmenting behavior and filtering, using
an OnKeyHandler
), you will have
to understand and override View
key event handling. To summarize the process, events are dispatched
to the View
through the DispatchKeyEvent
method. DispatchKeyEvent
implements the behavior
described previously, offering the event to the onKeyHandler
first. If the handler returns
false
, it offers the event to the
View
methods implementing the
KeyEvent.Callback
interface:
onKeyDown
, onKeyUp
, and onKeyMultiple
.
As demonstrated in Example 10-9 and Listening for Touch Events, MotionEvents
are delivered to the widget
whose bounding rectangle contains the coordinates of the touch that
generated them. It isn’t quite so obvious how to determine which
widget should receive a KeyEvent
.
In order to do this, the Android UI framework, like most other UI
frameworks, supports the concept of selection, or focus.
In order to accept focus, a widget’s
focusable attribute must be set to true. This
can be done using either an XML layout attribute (the EditText
Views in Example 10-3 have their focusable
attribute set to false) or the setFocusable
method, as shown in the first
line of Example 10-10. A user changes
which View
has focus using D-pad
keys or by tapping the screen when touch is supported.
When a widget is in focus, it usually renders itself with some
kind of highlighting to provide feedback that it is the current
target of keystrokes. For instance, when an EditText
widget is in focus, it is drawn
both highlighted and with a cursor at the text insert
position.
To receive notification when a widget enters or leaves focus,
install an OnFocusChangeListener
. Example 10-12 shows the listener needed to
add a focus-related feature to the demo program. It causes a
randomly positioned black dot to be added automatically to the
DotView
every now and then,
whenever it is in focus.
dotView.setOnFocusChangeListener(new OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (!hasFocus && (null != dotGenerator)) { dotGenerator.done(); dotGenerator = null; } else if (hasFocus && (null == dotGenerator)) { dotGenerator = new DotGenerator(dots, dotView, Color.BLACK); new Thread(dotGenerator).start(); } } });
There should be few surprises in the OnFocusChangeListener
. When the DotView
comes
into focus, it creates the DotGenerator
and spawns a thread to run
it. When the widget leaves focus, the DotGenerator
is stopped and freed. The new
data member, dotGenerator
(whose declaration
is not shown in the example), is nonnull only when the DotView
is in focus. There is an important and powerful new tool in
the implementation of DotGenerator
, and we’ll return to it in a
moment.
Focus is transferred to a particular widget by calling its
View
method, requestFocus
. When requestFocus
is called for a new target widget, the request is passed
up the tree, parent by parent, to the tree root. The root remembers
the widget that is in focus and passes subsequent key events to it
directly.
This is exactly how the UI framework changes focus to a new
widget in response to a D-pad keystroke. The framework identifies
the widget that will be in focus next and calls that widget’s
requestFocus
method. This causes
the previously focused widget to lose focus and the target to gain
it.
The process of identifying the widget that will gain focus is complicated. In order to do it, the navigation algorithm has to perform some tricky calculations that may depend on the locations of every other widget on the screen.
Consider, for instance, what happens when the right D-pad button is pressed and the framework attempts to transfer focus to the widget immediately to the right of the one that is currently in focus. When looking at the screen, it may be completely obvious which widget that is. In the View tree, however, it is not nearly so obvious. The target widget may be at another level in the tree and several branches away. Identifying it depends on the exact dimensions of widgets in yet other distant parts of the tree. Fortunately, despite the considerable complexity, the Android UI framework implementation usually just works as expected.
When it does not, there are four properties—set either by
application method or by XML attribute—that can be used to force the
desired focus navigation behavior: nextFocusDown
, nextFocusLeft
, nextFocusRight
, and nextFocusUp
. Setting one of these
properties with a reference to a specific widget will ensure that
D-pad navigation transfers focus directly to that widget when
navigating in the respective direction.
Another complexity of the focus mechanism is the distinction
that the Android UI framework makes between D-pad focus and touch focus, for devices with touch-sensitive screens. To understand
why this is necessary, recall that on a screen that does not accept
touch input, the only way to push a button is to focus on it, using
D-pad navigation, and then to use the center D-pad key to generate a
click. On a screen that does accept touch events, however, there is
never any reason to focus on a button. Tapping the button clicks it,
regardless of which widget happens to be in focus at the time. Even
on a touch-sensitive screen, however, it is still necessary to be
able to focus on a widget that accepts keystrokes—an EditText
widget, for instance—in order to
identify it as the target for subsequent key events. In order to
handle both kinds of focus correctly, you will have to look into
View
’s handling of FOCUSABLE_IN_TOUCH_MODE
, and the View
methods isFocusableInTouchMode
and isInTouchMode
.
In an application with multiple windows, there is at least one more twist in the focus mechanism: it is possible for a window to lose focus without notifying the currently in-focus widget that its focus has been lost. This makes sense when you think about it. If the out-of-focus window is brought back to the top, the widget that was in focus in that window will again be in focus, with no other action.
Consider entering a friend’s phone number into an address book
application. Suppose you momentarily switch back to a phone
application to refresh your memory of the last few digits of his
phone number. You’d be annoyed if, on returning to the address book,
you had to focus again on the EditText
box in which you’d been typing.
You expect the state to be just as you left it.
On the other hand, this behavior can have surprising side
effects. In particular, the implementation of the auto-dot feature
presented in Example 10-12 continues to
add dots to the DotView
even when
it is hidden by another window. If a background task should run only
when a particular widget is visible, that task must be cleaned up
when the widget loses focus, when the Window
loses focus, and when the Activity
is paused or stopped.
Most of the implementation of the focus mechanism is in the
ViewGroup
class, in methods such
as requestFocus
and requestChildFocus
. Should it be necessary
to implement an entirely new focus mechanism, you’ll need to look
carefully at these methods, and override them appropriately.
Leaving the subject of focus and returning to the
implementation of the newly added auto-dot feature, Example 10-13 contains the
implementation of DotGenerator
.
private final class DotGenerator implements Runnable { final Dots dots; final DotView view; final int color; private final Handler hdlr = new Handler(); private final Runnable makeDots = new Runnable() { public void run() { makeDot(dots, view, color); } }; private volatile boolean done; // Runs on the main thread DotGenerator(Dots dots, DotView view, int color) { this.dots = dots; this.view = view; this.color = color; } // Runs on the main thread public void done() { done = true; } // Runs on a different thread! public void run() { while (!done) { try { Thread.sleep(1000); } catch (InterruptedException e) { } hdlr.post(makeDots); } } }
Here are some of the highlights of the code:
Creates an android.os.Handler
object, the new
tool introduced in this section.
Creates a new anonymous Runnable
object. It is used to call
MakeDot
from the main thread
in item 4.
The Constructor for DotGenerator
. The DotGenerator
is created on the main
thread.
The dot generation loop generates a new dot about every second. This is the only part of the code that runs on a completely different thread.
A naïve implementation of DotGenerator
would simply call makeDot
directly within its run
method. Doing this wouldn’t be safe,
however, unless makeDot
was
thread-safe—and the Dots
and
DotView
classes were too, for
that matter. This would be tricky to get correct and hard to
maintain. In fact, the Android UI framework actually forbids access
to a View
from multiple threads.
Running the naive implementation would cause the application to fail
with an Android runtime error like this:
11-30 02:42:37.471: ERROR/AndroidRuntime(162): android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
To get around this restriction, DotGenerator
creates a Handler
object within its constructor. A
Handler
object is associated with
the thread on which it is created and provides safe, concurrent
access to a canonical event queue for that thread.
Because DotGenerator
creates a Handler
during its own
construction (which happens on the main thread), this Handler
is associated with the main
thread. Now DotGenerator
can
use the Handler
to enqueue a
Runnable
that calls makeDot
from the main thread. As you might
guess, it turns out that the main-thread event queue on which the
Handler
enqueues the Runnable
is exactly the same one that is
used by the UI framework. The call to makeDot
is dequeued and dispatched, like
any other UI event, in its proper order. In this case, that causes
its Runnable
to be run. makeDot
is called from the main thread and
the UI stays single-threaded.
This is a very important pattern for coding with the Android
UI framework. Whenever processing started on behalf of the user
might take more than a few milliseconds to complete, doing that
processing on the main thread might cause the entire UI to become
sluggish or, worse, to freeze for a long time. If the main
application thread does not service its event queue for a couple of
seconds, the Android OS will kill the application for being
unresponsive. The Handler
class
allows the programmer to avoid this danger by delegating slow or
long-running tasks to other threads, so that the main thread can
continue to service the UI. When a task completes, it uses a
main-thread Handler
to enqueue an
update for the UI.
The demo application takes a slight shortcut here: it enqueues
the creation of a new dot and its addition to the dot model on the
main thread. A more complex application might pass a main thread
Handler
to the Model on creation,
and provide a way for the UI to get a model-thread Handler
from the model. The main thread
would receive update events enqueued for it by the Model, using its
main-thread Handler
. The Model,
running in its own thread, would use the Looper
class to dequeue and dispatch
incoming messages from the UI.
Passing events between the UI and long-running threads in this way dramatically reduces the constraints required to maintain thread safety. In particular, note that if an enqueuing thread retains no references to an enqueued object, or if that object is immutable, no additional synchronization is necessary.
The final aspect of application
control we’ll cover in this chapter is the menu. Example 10-14 shows how to implement a simple
menu by overriding two Activity
methods.
@Override public boolean onCreateOptionsMenu(Menu menu) { menu.add(Menu.NONE, CLEAR_MENU_ID, Menu.NONE, "Clear"); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case 1: dotModel.clearDots(); return true; default: ; } return false; }
When this code is added to the TouchMe
class, clicking the device’s Menu
key will cause the application to present a menu (labeled “Clear” at
the bottom of the screen), as shown in Figure 10-7.
Clicking the Enter key or tapping the menu item again will clear the dot widget.
Interestingly, if you run this application, you will find that
while the added menu item works most of the time, it does not work
when the DotView
is in focus. Can
you guess why?
If you guessed that the problem is caused by the OnKeyListener
installed in the DotView
, you are correct! As
implemented in Example 10-15, the
listener swallows the menu key event by returning true
when it is clicked. This prevents the
standard View
processing of the
menu key keystroke. In order to make the menu work, the OnKeyListener
needs a new case, shown in
Example 10-15.
switch (keyCode) { case KeyEvent.KEYCODE_MENU: return false; // ...
The Android UI framework also supports contextual menus. A
ContextMenu
appears in response to
a long click in a widget that supports it. The code required to add a
contextual menu to an application is entirely analogous to that for
the options menu shown earlier
except that the respective methods are onCreateContextMenu
and onContextItemSelected
.
Additionally, one more call is required. In order to support
contextual menus, a widget must be assigned a View.OnCreateContextMenuListener
by calling
its View
method, setOnCreateContextMenuListener
. Fortunately,
since Activity
implements the
View.OnCreateContextMenuListener
interface, a common idiom looks like Example 10-16.
findViewById(R.id.ctxtMenuView).setOnCreateContextMenuListener(this);
Simply overriding the default, empty Activity
implementations of the context menu
listener methods will give your application a context menu.
This chapter has shown how the Android graphical interface works overall, and has given you the tools to manipulate its basic components: windows, Views, and events. The following chapter explains the most useful widgets Android makes available, and Chapter 12 shows you how to do your own graphics programming.
44.200.94.150