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 (see "AbsoluteLayout" on page 215), LinearLayout (see "LinearLayout" on page 209), and RelativeLayout (see "RelativeLayout" on page 216), 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 12-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. We'll start with the layout process; drawing will be described later in the section "Canvas Drawing" on page 226.

Example 12-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.

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.

In "Assembling a Graphical Interface" on page 161, for instance, 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 set MeasuredDimensions 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 12-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: measure Child, 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.

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 12-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.

0 0

Post a comment