#PerfMatters building a custom ViewGroup — Part 3
If building a custom View does not suit your needs, a great option to improving performance may be to create a custom ViewGroup which reduces the measure and layout passes that are needed on your Views when a screen update happens.
In Part 1, we build a custom HorizontalBarView in order to simplify code and improve performance. In Part 2, we saw how the building blocks of custom ViewGroups work and now, we’re going to try to use what we know to build our own ViewGroup and see if this gives us any performance saving.
As a reminder, this is what the widget looks like:
Building our custom ViewGroup
In our custom ViewGroup, we are going to take 3 Views and lay them out in in order to build our widget. Readers of previous articles may remember that the default View only took 2 TextView inside a LinearLayout, however, our ViewGroup will take 3. It may seem, that we have additional complexity in our ViewGroup and, hence the approach may be inherently worse. To refresh your memory, the HorizontalBarView widget using default Views was built the following way:
<LinearLayout
android:id=”@+id/container”
android:layout_width=”match_parent”
android:layout_height=”35dp”
android:orientation=”horizontal”> <TextView
android:id=”@+id/fillView”
android:layout_width=”0dp”
android:layout_height=”match_parent”
android:layout_weight=”0"
android:background=”@android:color/holo_blue_dark”
android:gravity=”center_vertical|right”
android:padding=”4dp”
android:textColor=”@android:color/white”/> <TextView
android:id=”@+id/emptyView”
android:layout_width=”0dp”
android:layout_height=”match_parent”
android:layout_weight=”100"
android:background=”@android:color/darker_gray”
android:gravity=”center_vertical|left”
android:padding=”4dp”
android:text=”0.00 Filled”
android:textColor=”@android:color/black”/>
</LinearLayout>
This may intuitively look like it would be more efficient than using 3 Views, however the approach does have it’s limitations as we will see later.
While the HorizontalHarView building using the default Views only uses 2 child Views, these two Views are TextViews which have a fairly complex to calculate weight attribute set on them. In our custom ViewGroup, we’ll use a 2 plain and simple View’s to represent the bars and a TextView to show the text. The TextView has no complex attributes set on it and is measured and positioned by us.
<com.example….HorizontalBarViewGroup
android:id=”@+id/custom”
android:layout_width=”wrap_content”
app:vg_text=”0.00 Filled”
app:vg_fillPercentage=”0"
android:layout_height=”35dp”> <View
android:layout_width=”match_parent”
android:layout_height=”match_parent”/> <View
android:layout_width=”match_parent”
android:layout_height=”match_parent”/> <TextView
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:visibility=”visible”/>
</com.example….HorizontalBarViewGroup>
We are also able to push details such as the text to display and the fill percentage on the custom ViewGroup and hence simplify our code for initializing and updating the View.
In order to build our Widget, our custom ViewGroup will assume that it has 3 children, first 2 can be a plain and simple View and the third will be a TextView (please note, that you can also draw the text yourself hence saving you a View).
Measure your children
The first step is to provide each child it’s width and height, based on the width and height of our ViewGroup. We will also take this opportunity to set the text and color of the bars.
//onMeasure()
int totalWidth = MeasureSpec.getSize(widthMeasureSpec);
int totalHeight = heightMeasureSpec;//Note:
// You want to cache the children,
// as this is an expensive operation.
// that step is not shown here for clarity.
final View fillChild = getChildAt(0);
final View emptyChild = getChildAt(1);
final TextView textChild = (TextView) getChildAt(2);fillChild.setBackgroundColor(mFillColor);
emptyChild.setBackgroundColor(mEmptyColor);//This is the bar fill
mFillWidth = (int) (totalWidth * (mValue / 100f));
if (fillChild != null) {
fillChild.measure(
MeasureSpec.makeMeasureSpec(mFillWidth, MeasureSpec.EXACTLY),
totalHeight);
}mEmptyWidth = totalWidth — mFillWidth;
if (emptyChild != null) {
emptyChild.measure(
MeasureSpec.makeMeasureSpec(mEmptyWidth, MeasureSpec.EXACTLY),
totalHeight);
}if (textChild != null) {
textChild.measure(MeasureSpec.makeMeasureSpec(totalWidth,
MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(totalHeight, MeasureSpec.AT_MOST));
}setMeasuredDimension(totalWidth, totalHeight);
We use setMeasuredDimension to set the width and height of our ViewGroup because it is faster than calling super.onMeasure() and super.onMeasure() will also measure the children which is something we don’t want.
We know the fill percentage and we are using it to calculate the width of the first child. Since we know the width of the first child, the second child which represents the empty area is going to be the total width minus the first width of the first child.
With the TextView, we can take 2 approaches, either use the Paint to calculate the width and the height of the text and set it yourself, or since it is a TextView, just tell it to measure itself using the AT_MOST attribute, which should effectively translate to wrap_content.
Laying out the children
Once we have the width of the first child, we know the starting and ending location of the first child as well. Since we know the width of the first child, the second child which represents the empty area is going to be the total width minus the first width of the first child.
When we calculate the width of the second child, we can calculate it’s starting location from the ending location of the first child and the ending location would be the width of the ViewGroup.
Finally, the TextView has to be position to the left, in the fill area, aligned right; if the fill area is larger than the text width, or else it has to be positioned on the right in the empty area, aligned left. We can take this moment to set the appropriate text color and position the TextView starting at the end of the fill area minus the TextView’s width.
You can see the entire code for the HorizontalBarViewGroup below.
Measuring Performance
The custom ViewGroup on average for 5 updates, performs around the 3ms mark; whereas the widget created using the custom ViewGroups takes around 6ms. Below is an example to the layout and measure times.
//CUSTOM WIDGET
D/HBVG: Measure Count = 8, Time = 1,747,811
D/HBVG: Layout Count = 8, Time = 616,093
D/View1: Measure Count = 8, Time = 35,835
D/View1: Layout Count = 8, Time = 7,603
D/View2: Measure Count = 8, Time = 11,458
D/View2: Layout Count = 8, Time = 6,822
D/TextView: Measure Count = 4, Time = 319,011
D/TextView: Layout Count = 4, Time = 9,114
D/Main: Time = 982,395
Total = 3,346,229ns//DEFAULT WIDGET
D/LinearLayout: Measure Count = 8, Time = 2,815,991
D/LinearLayout: Layout Count = 8, Time = 458,019
D/TextView1: Measure Count = 13, Time = 1,476,043
D/TextView1: Layout Count = 13, Time = 9,894
D/TextView2: Measure Count = 16, Time = 1,050,104
D/TextView2: Layout Count = 16, Time = 7,394
D/Main: Time = 3,195,157
Total = 6,469,167ns
The total time taken, is the time the top level view took (LinearLayout or HorizontalBarViewGroup) in its measure and layout passes plus the time taken on the Main thread for the calculations and operations to update the Views. The time displayed is cumulative of the number of measure and layout passes.
As you can see, with the default widget, there are far more layout and measure passes taking place. This is because we use the layout_weight attribute which usually requires at least 2 measure passes.
By calculating the measure and layout times ourselves, we have managed to half the amount of time it takes to layout the widget.
Other considerations & conclusion
The logic for displaying the text in the appropriate side proved to be surprisingly difficult. This is mostly because even after updating the weights for the fill and empty views, the API still returns the old measured width value because the layout pass had not taken place yet. As a result, we have to partially guess which side to display the text on. Take a look at how complex the logic is when compared to our custom View:
private void add(float count) {
LinearLayout.LayoutParams params =
(LinearLayout.LayoutParams) mFillView.getLayoutParams();
float currentWeight = params.weight;
float nextWeight = currentWeight + count;
if (nextWeight > 100) {
nextWeight = 0;
}
params.weight = nextWeight;
mFillView.setLayoutParams(params); LinearLayout.LayoutParams paramsEmpty =
(LinearLayout.LayoutParams) mEmptyView.getLayoutParams();
paramsEmpty.weight = 100 — params.weight;
mEmptyView.setLayoutParams(paramsEmpty); String text = String.format(“%.2f Filled”, nextWeight);
float textWidth = p.measureText(text);
if (textWidth < mFillView.getMeasuredWidth() &&
nextWeight > 0 || nextWeight > 50) {
mFillView.setText(text);
mEmptyView.setText(null);
} else {
mFillView.setText(null);
mEmptyView.setText(text);
}
}
While using the custom ViewGroup may be a little more work, ultimately, it not only simplifies you code and significantly improve performance, it also lets you achieve a result which may not be possible using the default widgets.
This was the final part in my #PerfMatters custom Views and ViewGroups series. Do read Part 1, Custom Views and Part 2, introduction to custom ViewGroups if you haven’t already.
One final thought. I’d like to leave you with is something a friend of mine said “Over optimization is the number one cause of bugs”. While performance is extremely important, it’s equally important to write code that is easy to read, extensible and manageable. Knowing when and how much of one to sacrifice for the other requires real talent.