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.
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.
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.
ViewGroup
parent of each View widget. This phase positions the View on the screen, relative to its parent ViewGroup
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.
New|Java Class
.
CircleLayout
.
Superclass
to android.view.ViewGroup
.
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);
}
onMeasure
method to calculate the size of the CircleLayout
and all of its child Viewwidgets
. The measurement specifications are passed in as int values, which are interpreted using the staticmethods
in the MeaureSpec
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 the CircleLayout
as the size given in the specification. This means that the CircleLayout
will always consume the maximum amount of space available. It also expects all of its children to be able to specify sizes without the match_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));
}
onLayout
method. This performs the actual arrangement of the child View widget within the CircleLayout
, 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 override onLayout
, but invoking layout.CircleLayoutassumes
that all the child View widgets are of the same size (and forces this as part of the onLayoutimplementation
). This onLayout
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.