Watch That Baseline Alignment

Dave Smith
Dave Smith
Watch That Baseline Alignment

I'll go out on a limb and say that every Android developer at one point or another uses LinearLayout to build a row of elements. Let’s take the following simple layout as an example of what I mean. A horizontal row of buttons all evenly spaced out across the container using weight.

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">
    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textSize="12dp"
        android:text="Last" />
    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textSize="12dp"
        android:text="Next" />
    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textSize="12dp"
        android:text="Reload" />
    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textSize="12dp"
        android:text="Exit" />
</LinearLayout>

Initial Layout

The weight system of LinearLayout makes using it to build rows with even spacing across the children simple and effective. However, as often as you may have done this, you might have also run into a perplexing issue that arises when the text of one or more elements inside the row gets long enough to force line wrapping.

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">
    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textSize="12dp"
        android:text="Last" />
    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textSize="12dp"
        android:text="Next" />
    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textSize="12dp"
        android:text="Reload Content" />
    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textSize="12dp"
        android:text="Exit" />
</LinearLayout>

Not only has the view content been shifted down, but the parent container isn’t sized to fit the larger height this creates, so part of the shifted view is clipped!

(╯°□°) ╯︵ ┻━┻

The first time you see this, it is a bit maddening because there is no apparent reason for what has just happened. If you’re like me, you’ll go through your mental checklist:

  • Did I miss a margin somewhere?
  • Is there extra padding in the view or the background image?
  • Does the view add extra padding to accommodate for multiple lines of text?
  • Why does it only happen to the view when the text wraps?

This behavior makes absolutely no sense, until you understand what is actually going on under the hood.

If we take a much closer look at the alignment of the child elements, something interesting emerges. The views are being laid out such that the first line of text is vertically aligned for each child. This is known as "baseline" alignment, and it just so happens LinearLayout has a flag to control this, and it’s enabled by default. In other words, LinearLayout (and its subclasses) will attempt to align child elements by their text baseline when possible.

Child elements subject to this are those that report a valid baseline; basically all the subclasses of TextView, but it could be any View that overrides and returns a positive value from getBaseline(). Because of this, you may encounter this behavior for any container widget that is built from LinearLayout (which includes TableLayout, RadioGroup, and SearchView) and child element based on TextView (Button, CheckBox, RadioButton, EditText, just to name a few). More often than not, I hit this condition in my own code building a grid of checkable boxes or a row of radio buttons.

Solution

In some cases, depending on your container height sizing, the solution can simply be to enforce centered gravity on the child elements by adding android:gravity="center_vertical" to the layout container. Although to some degree, even in cases where this works it is a case of solving the symptom rather than the cause. In this author’s humble opinion, a better solution is to disable baseline alignment for the container view when it is not useful for your application. This can be done in XML with android:baselineAligned="false" or in Java code via setBaselineAligned(false). Here is our previous example with the fix applied.

Fixed Layout

Hopefully this tip will save you from the many facepalms that my forehead was forced to endure.