#PerfMatters introduction to custom ViewGroups to improve performance — Part 2

The bundled ViewGroups such as LinearLayout and RelativeLayout are generic all purpose containers. This means that in order for them to figure out how to layout your Views, they may have to do multiple measurement and layout passes. The more Views you have and the deeper they are nested, the more the complexity and the more expensive a layout change can be time wise. If you know how your Views are going to be laid out within a container, you can improve a performance by measuring and laying out your Views yourself.

Ali Muzaffar
AndroidPub
Published in
6 min readSep 22, 2015

--

In part 1, I talked about how we can build our own View in order to avoid having multiple Views, reduce complexity, and hence improve performance by avoiding layout passes. In this article I’ll try to demystify onMeasure and onLayout methods in ViewGroup which you can use to measure and position all the children in your container.

How to set the dimensions of children in a ViewGroup

In this scenario, we want to ask the children of the ViewGroup to measure themselves and lay them out horizontally.

Step 1: Create a custom ViewGroup

Create a new class that extends ViewGroup. Then override the constructors.

public class HorizontalBarViewGroup extends ViewGroup {  public HorizontalBarViewGroup(Context context, 
AttributeSet attrs) {
super(context, attrs);
}
public HorizontalBarViewGroup(Context context,
AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public HorizontalBarViewGroup(Context context) {
super(context);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public HorizontalBarViewGroup(Context context,
AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onLayout(
boolean changed, int l, int t, int r, int b) {

}
}

You will also need to provide an implementation of the onLayout method. This method assigns a position and size to each of its children. Below I’ve provided a very basic implementation that iterates over all the children and lays them out one after the next horizontally.

@Override
protected void onLayout(
boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int prevChildRight = 0;
int prevChildBottom = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
child.layout(prevChildRight, prevChildBottom,
prevChildRight + child.getMeasuredWidth(),
prevChildBottom + child.getMeasuredHeight());
prevChildRight += child.getMeasuredWidth();
}
}

The code is straight forward. We loop through each child and give it a position starting from the ending position of the previous child. The parameters are:

  • Left — starting x-coordinate for the view
  • Top — starting y-coordinate for the view
  • Right — ending x-coordinate for the view
  • Bottom — ending y-coordinate for the view

Next, create an activity and use your ViewGroup in it’s layout:

<LinearLayout 
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/activity_vertical_margin"
tools:context=".MainActivity">
<com.example.alimuzaffar.perfmatters.HorizontalBarViewGroup
android:background="#80FF0000"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:background="#00FF00"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World"/>
<TextView
android:background="#0080FF"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World"/>
</com.example.alimuzaffar.perfmatters.HorizontalBarViewGroup>
</LinearLayout>

When you run the code, you’ll see that the custom ViewGroup fills the screen. The children are not visible. This is because we have not told the children how to measure themselves and consequently, they aren’t being rendered. Our ViewGroup does know how to measure itself, but we aren’t doing anything with this information, as a result, it’s filling all available space.

Step 2: Measure the children

We tell the children to measure themselves in the onMeasure() method, the simplest implementation of this is is to loop through the children and ask them using the measureChild() method.

@Override
protected void onMeasure(
int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}

Now if you run the code, you’ll see that the children render. The ViewGroup still takes up the full screen, but the children render as expected. You can simplify this code by using the measureChildren() method which will loop over all the children and ask them to measure themselves automatically. This method will also skip children that have visibility set to gone, hence supporting the visibility gone flag.

Currently, margins don’t work, which is perfectly fine. If we want to support margins, our container can add them in onLayout rather than trying to measure a child with the margins.

Step 3: Measure the container

So far, we have been telling the container to measure itself, but suppose we want to wrap_content on the container? In order to do this, we would need to know the total width of the children as well as the height of the tallest child. If we can calculate these 2 things, we can set the width and the height of the container using the method measureChild(). Go ahead and replace the code in onMeasure with the code below.
Note: you should not call super.onMeasure() now.

int totalWidth = 0;
int totalHeight = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
totalWidth += child.getMeasuredWidth();
if (child.getMeasuredHeight() > totalHeight) {
//height of the container, will be the largest height.
totalHeight = child.getMeasuredHeight();
}
}
setMeasuredDimension(totalWidth, totalHeight);

Just so you can see what the effect of the code, add padding to one of the children and run the code. You should see something like this:

The red area indicating our ViewGroup is exactly the width of the children and its the height of the tallest child.

Step 3: Make the children a particular size

If we want to save time, we can tell the children exactly what size we would like them to be. In this scenario, you would know the elements that are inside your ViewGroup and you know exactly what size (or at most what size) each is supposed to be and where it should be laid out. In order to tell the children how to lay themselves out, you can build your own MeasureSpec and pass it to the children.

If you put the following code in onMeasure, each child should be resize to be EXACTLY 3o0px:

int totalWidth = 0;
int totalHeight = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
int width = MeasureSpec.makeMeasureSpec(300,
MeasureSpec.EXACTLY);
int height = MeasureSpec.makeMeasureSpec(300,
MeasureSpec.EXACTLY);
child.measure(width, height);
totalWidth += width;
if (height > totalHeight) {
//height of the container, will be the largest height.
totalHeight = child.getMeasuredHeight();
}
}
setMeasuredDimension(totalWidth, totalHeight);

Again, each child is 300px and our ViewGroup should wrap them tightly.

As you can see, the second View still renders the padding we put around it as well.

Putting it all together

We now have some basic tools on how we can provide size and layout information to the child Views in a ViewGroup. You should already be bubbling with ideas on how you can use this to make layouts otherwise not possible or to improve rendering speeds for your layouts. In Part 3, we’ll take a look a look at creating the HorizontalBarView using our custom ViewGroup and see how that compares to using the default Views.

Yay! you made it to the end! We should hang out! feel free to follow me on LinkedIn, Google+ or Twitter.

--

--

Ali Muzaffar
AndroidPub

A software engineer, an Android, and a ray of hope for your darkest code. Residing in Sydney.