Creating a custom layout implementation for your Android app

0
2615
5 min read

In most applications, you’ll find that a combination of the
ConstraintLayoutCoordinatorLayout, 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):

  1. Right-click on the widget package in the travel claim example app, and select New|Java Class.
  2. Name the new class CircleLayout.
  3. Change the Superclass to android.view.ViewGroup.
  4. Click OK to create the new class.
  5. 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 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));
}
  • The next method to implement is the 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.

Hands on Android UI Development

 

Managing Editor, Packt Hub. Former mainframes/DB2 programmer turned marketer/market researcher turned editor. I love learning, writing and tinkering when I am not busy running after my toddler. Wonder how algorithms would classify this!

LEAVE A REPLY

Please enter your comment!
Please enter your name here