Monthly Archives: July 2013

Aligning Views Based on the Center of a RelativeLayout

In an effort to keep my View hierarchies simple, I have sometimes found myself using a little trick to layout Views on one half of a RelativeLayout container. The trick is to use a zero-size, centered View and then align your other Views based on that. Something as simple as this:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <View
        android:id="@+id/ctr"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_centerInParent="true" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/ctr"
        android:text="I am on the right half of the container!" />
</RelativeLayout>

Cool! However, there is a small caveat to this. If the RelativeLayout container has an inexact size (e.g. it has defined height/width as wrap_content), this layout trick will not work as expected. The TextView would end up aligned against the left edge of the container instead of in the right half of the container.

If we dig into the source code for RelativeLayout, we can find the cause of this problem in the onMeasure method. In a first pass through the views in the RelativeLayout, centered views are placed in the center of the container. However, if the RelativeLayout has a width/height that is wrap_content, centered views are placed along the left edge of the container. This is because we don’t yet know the width/height of the RelativeLayout container as we must wait for all of the child views to be measured. At the very end of the onMeasure method, child views that have requested to be centered are corrected since we now know the height/width of the RelativeLayout container. Unfortunately, child views that depend on the positioning of these centered views are not further adjusted. So, the child views that are dependent upon the positioning of a centered view end up being positioned based upon the centered view’s location when it was originally aligned against the left edge of the container.

Let’s see how this might look:

The mess on the left is caused by overlapping TextViews that were supposed to have been aligned (and not overlapped) based on a centered View.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:layout_marginTop="16dp"
    android:background="#333"
    android:padding="@dimen/activity_horizontal_margin" >

    <TextView
        android:id="@+id/house"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:includeFontPadding="false"
        android:text="@string/house_name"
        android:textColor="#d4d5d6"
        android:textSize="43sp" />

    <TextView
        android:id="@+id/castle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/house"
        android:layout_marginBottom="16dp"
        android:text="@string/castle"
        android:textColor="#727887"
        android:textSize="14sp" />

    <TextView
        android:id="@+id/heir"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/castle"
        android:text="@string/heir1"
        android:textAllCaps="true"
        android:textColor="#727887"
        android:textSize="12sp" />

    <TextView
        android:id="@+id/heir_detail"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@id/heir"
        android:layout_below="@id/heir"
        android:drawablePadding="4dp"
        android:text="@string/heir1_title"
        android:textColor="#d4d5d6"
        android:textSize="14sp"
        android:visibility="visible" />

    <View
        android:id="@+id/ctr"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_centerInParent="true" />

    <TextView
        android:id="@+id/heir2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignTop="@id/heir"
        android:layout_toRightOf="@id/ctr"
        android:text="@string/heir2"
        android:textAllCaps="true"
        android:textColor="#727887"
        android:textSize="12sp" />

    <TextView
        android:id="@+id/heir2_detail"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@id/heir2"
        android:layout_below="@id/heir2"
        android:text="@string/heir2_title"
        android:textColor="#d4d5d6"
        android:textSize="14sp" />

</RelativeLayout>

There are several ways to fix this and get the output we really want, none of which are necessarily ideal.

The easy fix, if you can do it, is to simply change the wrap_content height/width to match_parent or a specific size (e.g. 100dp). Obviously, that will not always give you appropriate results.

A second option would be to change to some other container, like a LinearLayout. This will likely increase the depth of your view hierarchy, making your layout heavier and the layout pass take longer.

Finally, a simple extended version of RelativeLayout with an altered onMeasure method can give you the desired results at the expense of a second measure pass. By calling the super onMeasure method a second time, passing exact measurements, we can leverage the existing RelativeLayout code to do the hard work for us and give us the results we desire:

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		
		if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY
				|| MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
			super.onMeasure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
					MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
		}
	}

Full source for this “DoubleMeasureRelativeLayout” is available.

By simply swapping out the RelativeLayout with this DoubleMeasureRelativeLayout, the desired output is easily obtained:

doublemeasure_relative_layout

With the DoubleMeasurRelativeLayout, the TextViews are lined up in the expected location and the result is much better.