mattias | niklewski.com

I Cut, You Choose: Dividing a Layout by Percent

Android does not support percent values for the layout_height and layout_width attributes. Although the android.widget documentation is curiously silent on the topic, there is a trick involving LinearLayout that works more or less like you would expect.

The trick is described on Stackoverflow and elsewhere. Let's see how it works, and also why it sometimes isn't exactly what you need.

As an example, let's use a horizontal LinearLayout with 3 children. We want the children to occupy 30, 50 and 20 percent of the available width. The `layout_weight" attribute sounds promising, so here is an initial attempt.

<LinearLayout
    a:layout_width="match_parent"
    a:layout_height="wrap_content" >

    <Button
        a:layout_width="wrap_content"
        a:layout_height="wrap_content"
        a:layout_weight="30"
        a:text="1st" />

    <Button
        a:layout_width="wrap_content"
        a:layout_height="wrap_content"
        a:layout_weight="50"
        a:text="2nd" />

    <Button
        a:layout_width="wrap_content"
        a:layout_height="wrap_content"
        a:layout_weight="20"
        a:text="3rd" />

</LinearLayout>

This looks almost right, but if you measure carefully you'll see that it's off by a small amount. This is because layout_weight distributes only the excess space -- whatever space remains after the child views are measured.

The difference becomes more noticable if we vary the text lengths.

As one view grows, there is less excess space to distribute and the remaining views shrink. Note that although unhelpful to us right now, this is often a useful behaviour. It fits well with the Android philosophy that a single layout should work for a variety of screens and languages. On a more practical note: when you override it, it becomes your responsibility to ensure that there is enough space for the Finnish translation.

Leaving the Scandinavian market aside, how do we override LinearLayouts weight algoritm? Just set the width of each button to 0dp.

<LinearLayout
    a:layout_width="match_parent"
    a:layout_height="wrap_content" >

    <Button
        a:layout_width="0dp"
        a:layout_height="wrap_content"
        a:layout_weight="30"
        a:text="1st" />

    <Button
        a:layout_width="0dp"
        a:layout_height="wrap_content"
        a:layout_weight="50"
        a:text="2nd" />

    <Button
        a:layout_width="0dp"
        a:layout_height="wrap_content"
        a:layout_weight="20"
        a:text="3rd" />

</LinearLayout>

This tells LinearLayout to allocate no space at all to the children to begin with. All the available space becomes excess space. And since the excess space is distributed according to the weights, as we saw earlier, the result is exactly right.

Not exactly right, I guess. Somewhere in Finland they are laughing at me. The split is perfect, but the text no longer fits on one line. And what's with the misaligned pink view?

Look at that last image again, and pay attention to the vertical alignment. The word "thirty" is perfectly aligned with "50%" and "20%". This is no coincidence. When LinearLayout is in horizontal mode, it uses getBaseline() on its children and tries to baseline align them. The algorithm did its best, but our uncooperative 2-line text is making life difficult for it.

If we tweak the font sizes a little bit, we can appreciate the baseline alignment when it's working as intended.

Baseline alignment is a beautiful thing, when you need it. Which turns out to be not very often. The rest of the time it is annoying, messing up your layout often in subtle ways. Luckily it can be turned off by setting baselineAligned to false.

This is the final version of the xml.

<LinearLayout
    a:layout_width="match_parent"
    a:layout_height="wrap_content"
    a:baselineAligned="false" >

    <Button
        a:layout_width="0dp"
        a:layout_height="wrap_content"
        a:layout_weight="30"
        a:text="1st" />

    <Button
        a:layout_width="0dp"
        a:layout_height="wrap_content"
        a:layout_weight="50"
        a:text="2nd" />

    <Button
        a:layout_width="0dp"
        a:layout_height="wrap_content"
        a:layout_weight="20"
        a:text="3rd" />

</LinearLayout>

If you run into problems like those we saw earlier, or if you are curious about how a LinearLayout is measured, you should take a look at the source code.

Here's a link to the entry point for horizontal mode, and here's a comment that talks about the 0dp trick. This trick not only provides fake percent support, it is slightly more efficient too -- though only if you turn off baseline alignment. Maybe you have seen Android's lint tool nag you to turn off baseline alignment? It is trying to save you from a potentially expensive call to measure().

One final note about android:weightSum, since it is often mentioned along with layout_weight. There is one valid use for it; in case you need to leave some of the excess space unallocated - example. In every other case, it is better to let LinearLayout calculate the weight sum automatically.