“widget” is a just convenient term for a
subclass of android.view.View,
typically for a leaf node in the view tree. Many views are just
containers for other views and are used for layout; we don’t consider them
widgets, because they don’t directly interact with the user, handle
events, etc. So the term “widget,” although informal, is useful for
discussing the workhorse parts of the user interface that have the
information and the behavior users care about.
Similarly, the MicroJobs application has a view that contains a list
of names corresponding to locations on a map. As additional locations are
added to the map, new name-displaying widgets are added dynamically to the
list. Even this dynamically changing layout is just a use of pre-existing
widgets; it does not create new ones.
A very complex widget, perhaps used as an interface tool implemented in several
places (even by multiple applications), might even be an entire package of
classes, only one of which is a descendant of View.
1. Layout
Most of the heavy lifting in the Android framework layout
mechanism is implemented by container views. A container
view is one that contains other views. It is an internal node in the
view tree and subclasses of ViewGroup
(which, in turn, subclasses View). The framework toolkit provides a variety of sophisticated
container views that provide powerful and adaptable strategies for
arranging a screen. AbsoluteLayout, LinearLayout , and RelativeLayout, to
name some common ones, are container views that are both relatively easy
to use and fairly hard to reimplement correctly. Since they are already
available, fortunately you are unlikely to have to implement most of the
algorithm discussed here. Understanding the big picture, though—how the framework manages the
layout process—will help you build correct, robust widgets.
Example 1 shows a widget
that is about as simple as it can be, while still working. If added to
some Activity’s view tree, this widget will fill in the space allocated
to it with the color cyan. Not very interesting, but before we move on
to create anything more complex, let’s look carefully at how this
example fulfills the two basic tasks of layout and drawing.
Example 1. A trivial widget
public class TrivialWidget extends View {
public TrivialWidget(Context context) { super(context); setMinimumWidth(100); setMinimumHeight(20); }
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension( getSuggestedMinimumWidth(), getSuggestedMinimumHeight()); }
@Override protected void onDraw(Canvas canvas) { canvas.drawColor(Color.CYAN); } } |
Dynamic layout is necessary because the space requirements for
widgets change dynamically. Suppose, for instance, that a widget in a
GPS-enabled application displays the name of the city in which you are
currently driving. As you go from “Ely” to “Post Mills,” the widget
receives notification of the change in location. When it prepares to
redraw the city name, though, it notices that it doesn’t have enough
room for the whole name of the new town. It needs to request that the
screen be redrawn in a way that gives it more space, if that is
possible.
Layout can be a surprisingly complex task and very difficult to
get right. It is probably not very hard to make a particular leaf widget
look right on a single device. On the other hand, it can be very tricky
to get a widget that must arrange children to look right on multiple
devices, even when the dimensions of the screen change.
Layout is initiated when the requestLayout method is invoked on some view
in the view tree. Typically, a widget calls requestLayout on itself when it needs more
space. The method could be invoked, though, from any place in an
application, to indicate that some view in the current screen no longer
has enough room to draw itself.
The requestLayout method causes
the Android UI framework to enqueue an event on the UI event queue. When
the event is processed, in order, the framework gives every container
view an opportunity to ask each of its child widgets how much space each
child would like for drawing. The process is separated into two phases:
measuring the child views and then arranging them in their new
positions. All views must implement the first phase, but the second is
necessary only in the implementations of container views that must
manage the layout of child views.
1.1. Measurement
The goal of the measurement phase is to provide each view with an opportunity to
dynamically request the space it would ideally like for drawing. The
UI framework starts the process by invoking the measure method of the view at the root of
the view tree. Starting there, each container view asks each of its children how much space
it would prefer. The call is propagated to all descendants, depth
first, so that every child gets a chance to compute its size before
its parent. The parent computes its own size based on the sizes of its
children and reports that to its parent, and so on, up the
tree.
The topmost LinearLayout asks each of the
nested LinearLayout widgets for its
preferred dimensions. They in turn ask the Buttons
or EditText views they contain for
theirs. Each child reports its desired size to its parent. The parents
then add up the sizes of the children, along with any padding they
insert themselves, and report the total to the topmost LinearLayout.
Because the framework must guarantee certain behaviors for all
Views, during this process, the measure method is final and cannot be
overridden. Instead, measure calls
onMeasure, which widgets may
override to claim their space. In other words, widgets cannot override
measure, but they can override
onMeasure.
The arguments to the onMeasure method describe the space the
parent is willing to make available: a width specification and a
height specification, measured in pixels. The framework assumes that no view will
ever be smaller than 0 or bigger than 230
pixels in size and, therefore, it uses the high-order bits of the
passed int parameter to encode the
measurement specification mode. It is as if onMeasure
were actually called with four arguments: the width specification
mode, the width, the height specification mode, and the height. Do not
be tempted to do your own bit-shifting to separate the pairs of arguments! Instead, use the static
methods MeasureSpec.getMode and
MeasureSpec.getSize.
The specification modes describe how the container view wants
the child to interpret the associated size. There are three of
them:
MeasureSpec.EXACTLY
The calling container view has already determined the exact size of the
child view.
MeasureSpec.AT_MOST
The calling container view has set a maximum size for this dimension,
but the child is free to request less.
MeasureSpec.UNSPECIFIED
The calling container view has not imposed any limits on the
child, and so the child may request anything it chooses.
A widget is always responsible for telling its parent in the
view tree how much space it needs. It does this by calling setMeasuredDimensions to set the properties that then become available to the parent,
through the methods getMeasuredHeight and getMeasuredWidth. If your
implementation overrides onMeasure
but does not call setMeasuredDimensions,
the measure method
will throw IllegalStateException
instead of completing normally.
The default implementation of onMeasure,
inherited from View, calls setMeasuredDimensions with one of two
values, in each direction. If the parent specifies MeasureSpec.UNSPECIFIED, it uses the
default size of the view: the value supplied by either getSuggestedMinimumWidth or
getSuggestedMinimumHeight. If the
parent specifies either of the other two modes, the default
implementation uses the size that was offered by the parent. This is a
very reasonable strategy and allows a typical widget implementation to
handle the measurement phase completely, simply by setting the values
returned by getSuggestedMinimumWidth and getSuggestedMinimumHeight.
Your widget may not actually get the space it requests. Consider
a view that is 100 pixels wide and has three children. It is probably
obvious how the parent should arrange its children if the sum of the
pixel widths requested by the children is 100 or less. If, however,
each child requests 50 pixels, the parent container view is not going
to be able to satisfy them all.
A container view has complete control of how it arranges its
children. In the circumstance just described, it might decide to be
“fair” and allocate 33 pixels to each child. Just as easily, it might
decide to allocate 50 pixels to the leftmost child and 25 to each of
the other two. In fact, it might decide to give one of the children
the entire 100 pixels and nothing at all to the others. Whatever its
method, though, in the end the parent determines a size and location
for the bounding rectangle for each child.
Another example of a container view’s control of the space
allocated to a widget comes from the example widget shown previously
in Example 1. It always
requests the amount of space it prefers, regardless of what it is
offered (unlike the default implementation). This strategy is handy to
remember for widgets that will be added to the toolkit containers,
notably LinearLayout, that
implement gravity. Gravity is a property that some views use to specify the
alignment of their subelements. The first time you use one of these
containers, you may be surprised to find that, by default, only the
first of your custom widgets gets drawn! You can fix this either by
using the setGravity method to
change the property to Gravity.FILL
or by making your widgets insistent about the amount of space they
request.
It is also important to note that a container view may call a
child’s measure method several
times during a single measurement phase. As part of its implementation
of onMeasure, a clever container
view, attempting to lay out a horizontal row of widgets, might call
each child widget’s measure method
with mode MEASURE_SPEC.UNSPECIFIED and a
width of 0 to find out what size the widget would prefer. Once it has
collected the preferred widths for each of its children, it could
compare the sum to the actual width available (which was specified in
its parent’s call to its measure
method). Now it might call each child widget’s measure method again, this time with the
mode MeasureSpec.AT_MOST and a width
that is an appropriate proportion of the space actually available.
Because measure may be called
multiple times, an implementation of onMeasure
must be idempotent and must not change the application
state.
A container view’s implementation of onMeasure is likely to be fairly complex.
ViewGroup, the superclass of all
container views, does not supply a default implementation. Each of the
UI framework container views has its own. If you contemplate
implementing a container view, you might consider basing it on one of
them. If, instead, you implement measurement from scratch, you are
still likely to need to call measure for each child and should consider
using the ViewGroup helper
methods: measureChild, measureChildren, and measureChildWithMargins. At the conclusion
of the measurement phase, a container view, like any other widget,
must report the space it needs by calling setMeasuredDimensions.
1.2. Arrangement
Once all the container views in the view tree have had a chance
to negotiate the sizes of each of their children, the framework begins
the second phase of layout, which consists of arranging the children.
Again, unless you implement your own container view, you probably will
never have to implement your own arrangement code. This section
describes the underlying process so that you can better understand how
it might affect your widgets. The default method, implemented in
View, will work for typical leaf widgets, as
demonstrated previously by Example 1.
Because a view’s onMeasure
method might be called several times, the framework must use a
different method to signal that the measurement phase is complete and
that container views must fix the final locations of their children.
Like the measurement phase, the arrangement phase is implemented with
two methods. The framework invokes a final method, layout, at the top of the view tree. The
layout method performs processing
common to all views and then delegates to onLayout, which custom widgets override to
implement their own behaviors. A custom implementation of onLayout must at
least calculate the bounding rectangle that it will supply to each
child when it is drawn and, in turn, invoke the layout method for each child (because it
might also be a parent to other widgets).
It is worth reiterating that a widget is not guaranteed to
receive the space it requests. It must be prepared to draw itself in
whatever space is actually allocated to it. If it attempts to draw
outside the space allocated to it by its parent, the drawing will be
clipped by the clip rectangle. To exert fine control—to fill exactly
the space allocated to it, for instance—a widget must either implement
onLayout and record the dimensions
of the allocated space or inspect the clip rectangle of the Canvas that is the parameter to onDraw.