In most applications, you’ll find that a combination of the
ConstraintLayout
, CoordinatorLayout
, and some of the more primitive layout classes (such as LinearLayout
and FrameLayout
) are more than enough to achieve any layout requirements you can dream up for your user interface. Every now and again though, you’ll find yourself needing a custom layout manager to achieve an effect required for the application.
Layout classes extend from the ViewGroup
class, and their job is to tell their child widgets where to position themselves, and how large they should be. They do this in two phases: the measurement phase and the layout phase.
All View implementations are expected to provide measurements for their actual size according to specifications. These measurements are then used by the View widget’s parent
ViewGroup
to allocate the amount of space the widget will consume on the screen. For example, a View might be told to consume, at most, the screen width. The View must then determine how much of that space it actually requires, and records that size in its measured dimensions. The measured dimensions are then used by the parent ViewGroup during the layout process.
The second phase is the layout phase, and it is conducted by the ViewGroup
parent of each View widget. This phase positions the View on the screen, relative to its parent ViewGroup
location, and specifies the actual size that the widget will consume on the screen (typically based on the measured size calculated in the measurement phase).
When you implement your own ViewGroup
, you’ll need to ensure that all of your child View widgets are given a chance to measure themselves before you perform the actual layout operations.
Let’s build a layout class to arrange its children in a circle. To keep the implementation simple, we’ll assume that all the child widgets are the same size (for example, if they were all icons):
- Right-click on the widget package in the travel claim example app, and select
New|Java Class
. - Name the new class
CircleLayout
. - Change the
Superclass
toandroid.view.ViewGroup
. - Click OK to create the new class.
- Declare the standard
ViewGroup
constructors:
public CircleLayout(final Context context) {
super(context);
}
public CircleLayout(
final Context context,
final AttributeSet attrs) {
super(context, attrs);
}
public CircleLayout(
final Context context,
final AttributeSet attrs,
final int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
- Override the
onMeasure
method to calculate the size of theCircleLayout
and all of its childViewwidgets
. The measurement specifications are passed in as int values, which are interpreted using thestaticmethods
in theMeaureSpec
class. Measurement specifications come in two flavors: at most and exactly, and each has a size value attached. In this particular layout, we always measure theCircleLayout
as the size given in the specification. This means that theCircleLayout
will always consume the maximum amount of space available. It also expects all of its children to be able to specify sizes without thematch_parent
attribute (as this will cause each child to take up all the available space):
@Override
protected void onMeasure(
final int widthMeasureSpec,
final int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(
MeasureSpec.getSize(widthMeasureSpec),
MeasureSpec.getSize(heightMeasureSpec));
}
- The next method to implement is the
onLayout
method. This performs the actual arrangement of the child View widget within theCircleLayout
, by invoking their layout method. The layout method should never be overridden, because it’s closely tied to the platform and performs several other important actions (such as notifying layout listeners). Instead, you should overrideonLayout
, but invokinglayout.CircleLayoutassumes
that all the child View widgets are of the same size (and forces this as part of theonLayoutimplementation
). ThisonLayout
method simply calculates the available space, and then positions the child View widgets in a circle around the outside edge:
protected void onLayout(
final boolean changed,
final int left,
final int top,
final int right,
final int bottom) {
final int childCount = getChildCount();
if (childCount == 0) {
return;
}
final int width = right - left;
final int height = bottom - top;
// if we have children, we assume they're all the same size
final int childrenWidth = getChildAt(0).getMeasuredWidth();
final int childrenHeight = getChildAt(0).getMeasuredHeight();
final int boxSize = Math.min(
width - childrenWidth,
height - childrenHeight);
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final double x = Math.sin((Math.PI * 2.0)
* ((double) i / (double) childCount));
final double y = -Math.cos((Math.PI * 2.0)
* ((double) i / (double) childCount));
final int childLeft = (int) (x * (boxSize / 2))
+ (width / 2) - (childWidth / 2);
final int childTop = (int) (y * (boxSize / 2))
+ (height / 2) - (childHeight / 2);
final int childRight = childLeft + childWidth;
final int childBottom = childTop + childHeight;
child.layout(childLeft, childTop, childRight, childBottom);
}
}
Although the implementation of the onLayout
method is quite long, it’s also relatively simple. Most of the code is concerned with determining the desired position of the child View widgets. Layout code needs to execute as quickly as possible, and should avoid allocating any objects during the onMeasure
and onLayout
methods (similar to the rules of onDraw
). Layout is a critical part of building the screen from a performance standpoint, because no rendering can actually occur without the layout being completed. The layout will also be rerun every time the layout changes its structure. For example, if you add or remove any child View widgets, or change the size or position of the ViewGroup
. Changing the size of a ViewGroup
might happen on every frame if you use a CoordinatorLayout
, where the ViewGroup
is being collapsed (or if you change its size as part of a property-animation).
You read an excerpt from the book, Hands-On Android UI Development by Jason Morris. For more recipes on cutting edge Android UI tasks such as creating themes, animations, custom widgets and more, give this book a try.