To Center A Button

Carloss Sessa of 50 Android Hacks starts off with an interesting problem: how to center a button that is 50% of its parent width?

Pretty cool Android phone, huh?

It’s easy to center a generic button. Using FrameLayout, one could simply set the appropriate layout gravity:

<FrameLayout android:layout_width="match_parent"
             android:layout_height="match_parent">

  <Button android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_gravity="center"
          android:text="Button"/>

</FrameLayout>

But how to set the button width to 50% of its parent? I decided to create a custom Button.

When creating custom views, you usually want to override some standard methods the framework uses. There are two I could consider in this case:

Because the parent layout already provides the position I’m after, onMeasure will suffice.

public class CenteredButton extends Button {
  // Constructors ommited for clarity.

  @Override protected void onMeasure(int wMeasureSpec, int hMeasureSpec) {
    super.onMeasure(wMeasureSpec, hMeasureSpec);

    View parent = (View) getParent();
    int halfParentWidth = parent.getWidth() / 2;

    setMeasuredDimension(halfParentWidth, getMeasuredHeight());
  }
}

The custom onMeasure is straightforward: after invoking super, which provides initial measurements, we get the parent width, halve it and apply the result. Then, we tell the XML layout to use it:

<FrameLayout android:layout_width="match_parent"
             android:layout_height="match_parent">

  <path.to.CenteredButton android:layout_width="wrap_content"
                          android:layout_height="wrap_content"
                          android:layout_gravity="center"
                          android:text="Button"/>

</FrameLayout>

It works! However, there’s a slight hiccup - the text is not centered. Applying android:gravity="center" doesn’t work:

"Button" text is not centered.

Darn super.onMeasure! A closer look at the source code reveals a simple text like “Button” is drawn with the help of a BoringLayout. The layout is also responsible for positioning the text based on the available height, width and gravity.

Since I only change the width after super.onMeasure, it has no effect to the initially calculated dimensions and position.

Enter MeasureSpec. Scary documentation hides its simple nature: the three modes, AT_MOST, UNSPECIFIED and EXACTLY, are a rough translation of the familiar wrap_content, match_parent and a specific size, e.g. 200dp.

Two encoded values of the onMeasure method, widthMeasureSpec and heightMeasureSpec present the MeasureSpec values from its parent View. This is handy, we can easily extract the parent width from it:

final int parentWidth = MeasureSpec.getSize(widthMeasureSpec);

If we could modify the widthMeasureSpec to be half its parent width, and pass it to the super.onMeasure, our work would be done. Fortunately, that’s easy:

@Override
protected void onMeasure(int wMeasureSpec, int hMeasureSpec) {
  int widthSize = MeasureSpec.getSize(wMeasureSpec);
  int halfWidth = widthSize / 2;
  int newMeasureSpec =
    MeasureSpec.makeMeasureSpec(halfWidth, MeasureSpec.EXACTLY);

  super.onMeasure(newMeasureSpec, hMeasureSpec);
}

Mission accomplished!