Layout Managers in Android: Relative and Constraint Layouts

In my last article, I showed off the most basic of the layout managers for Android – the LinearLayout and the TableLayout. With just these two layout managers, most layouts are possible. The drawback is that you have to write huge embedded layouts (layouts within layouts) to get the desired look.

Today I’m going to produce the same layout as before but with a much shallower tree of layouts. This means that the complexity of the XML is reduced and will result in a much smaller layout that is more readable.

RelativeLayout

The first layout manager I am going to discuss is the RelativeLayout. This is described on the Android site as “if you find yourself using lots of embedded linear layouts, this is the solution.” That’s a pretty bold claim, so let’s check it out.

In the LinearLayout, you specified the order of the widgets and their orientation. You also had three options for sizing – an absolute size, the minimum size (enough for the content) and a maximum size (the size of the parent).

In a TableLayout, you have a grid layout – columns and rows. You also have three options for sizing – an absolute size, the minimum size, and “the rest of the space”, allowing you to balance the size of the columns equally or take up the rest of the space in the parent.

In a RelativeLayout, you specify the position based on an edge or corner of the parent, or adjacent to another widget on the page. For example, our top three parts are always going to be glued together. We can specify that the top most widget is against the top edge of the parent, and then the current conditions is below that and then the location is below that.

Let’s see how we get along with this:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/background"
    tools:context="com.shellmonger.relativelayoutexample.MainActivity">
    <ImageView
        android:id="@+id/current_conditions_icon"
        android:src="@drawable/snow-icon-14"
        android:contentDescription="@string/default_conditions"
        android:paddingTop="24dp"
        android:paddingBottom="24dp"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <TextView
        android:id="@+id/current_conditions_text"
        android:text="@string/default_conditions"
        android:textSize="40sp"
        android:textColor="@android:color/white"
        android:fontFamily="sans-serif-light"
        android:paddingTop="12dp"
        android:paddingBottom="6dp"
        android:layout_below="@id/current_conditions_icon"
        android:layout_centerHorizontal="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <TextView
        android:id="@+id/current_location"
        android:text="@string/default_location"
        android:textSize="24sp"
        android:textColor="@android:color/white"
        android:fontFamily="sans-serif-light"
        android:paddingTop="12dp"
        android:paddingBottom="18dp"
        android:layout_below="@id/current_conditions_text"
        android:layout_centerHorizontal="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <TableLayout
        android:background="@color/futurecast"
        android:layout_below="@id/current_location"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:stretchColumns="1,2,3">
            <LinearLayout
                android:id="@+id/orange_layout"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@color/futurecast"
                android:paddingStart="8dp"
                android:paddingEnd="8dp"
                android:orientation="vertical">
                <TextView
                    android:id="@+id/current_temperature"
                    android:text="17°"
                    android:textColor="@android:color/white"
                    android:textSize="20sp"
                    android:paddingBottom="8dp"
                    android:fontFamily="sans-serif-light"
                    android:gravity="center_horizontal"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content" />
                <LinearLayout
                    android:id="@+id/minmaxtemp_layout"
                    android:layout_gravity="center_horizontal"
                    android:paddingTop="8dp"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content">
                    <TextView
                        android:text="@string/low_temp_label"
                        android:textColor="@android:color/white"
                        android:fontFamily="sans-serif-light"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content" />
                    <TextView
                        android:id="@+id/min_temperature"
                        android:text="15°"
                        android:textColor="@android:color/white"
                        android:fontFamily="sans-serif-light"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content" />
                    <TextView
                        android:text="@string/high_temp_label"
                        android:textColor="@android:color/white"
                        android:fontFamily="sans-serif-light"
                        android:paddingStart="4dp"
                        android:paddingEnd="2dp"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content" />
                    <TextView
                        android:id="@+id/max_temperature"
                        android:text="18°"
                        android:textColor="@android:color/white"
                        android:fontFamily="sans-serif-light"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content" />
                </LinearLayout>
            </LinearLayout>
            <TextView
                android:id="@+id/day1_name"
                android:text="@string/default_day1"
                android:textColor="@android:color/white"
                android:fontFamily="sans-serif-light"
                android:layout_gravity="center_horizontal"
                android:drawableBottom="@drawable/snow-icon-14"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" />
            <TextView
                android:id="@+id/day2_name"
                android:text="@string/default_day2"
                android:textColor="@android:color/white"
                android:fontFamily="sans-serif-light"
                android:layout_gravity="center_horizontal"
                android:drawableBottom="@drawable/snow-icon-14"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" />
            <TextView
                android:id="@+id/day3_name"
                android:text="@string/default_day3"
                android:textColor="@android:color/white"
                android:fontFamily="sans-serif-light"
                android:layout_gravity="center_horizontal"
                android:drawableBottom="@drawable/snow-icon-14"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" />
    </TableLayout>
    <ImageView
        android:id="@+id/info_icon"
        android:src="@android:drawable/ic_menu_info_details"
        android:contentDescription="@string/information"
        android:paddingStart="6dp"
        android:paddingEnd="6dp"
        android:layout_alignParentBottom="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <TextView
        android:id="@+id/switcher"
        android:text="@string/threedots"
        android:paddingStart="6dp"
        android:paddingEnd="6dp"
        android:textColor="@android:color/white"
        android:textSize="24sp"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:ignore="RelativeOverlap" />
    <ImageView
        android:id="@+id/add_button"
        android:src="@android:drawable/ic_menu_add"
        android:contentDescription="@string/add_location"
        android:paddingStart="6dp"
        android:paddingEnd="6dp"
        android:layout_alignParentEnd="true"
        android:layout_alignParentBottom="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</RelativeLayout>

You specify the position relative to those around it. You will see some of the following:

  • layout_(below|toLeftOf|toRightOf|above) – relative to the specified component (by id).
  • layout_alignParent(Bottom|Top|Start|End) – relative to the inside edge of the parent layout
  • layout_center(Vertical|Horizontal) – center on the parent

In the original (with both LinearLayout and TableLayout), this same layout took 230 lines. In the RelativeLayout version, there are 160 lines of code. In addition, the original version had a layout depth of 5. In the new version, we have a layout depth of 3 and that is due to the fact that I can’t figure out how to do the table/linear layout bit for the banner in the middle. More importantly, the XML is much more readable. I know where each element is relative to the ones around it.

That actually brings me to the bad part of RelativeLayout – and it’s minor. It’s actually hard to work out what needs to be done. I can visualize the zones in the Linear/Table layout version, but I can’t necessarily do that on paper with the RelativeLayout. Maybe this is just me. Once I have worked out how to do it, it’s obvious. Getting to that point is difficult for me – especially where I want to center things relative to their containing boxes.

It’s also worthwhile noting that grids (the same problem we solved with the TableLayout last time) are still a problem. However, the places where we need grids is less. For example, I replaced the lower controls section with some RelativeLayout positioning rather than including an embedded TableLayout since I could describe the positions relative to the parent. However, the future weather grid, where I place three columns equally, could not be replaced.

ConstraintLayout

ConstraintLayout (in the words of the docs) allows you to create large and complex layouts with a flat view hierarchy and no nested view groups. You can actually convert a RelativeLayout to a ConstraintLayout automatically. For each view, you need to define a horizontal and vertical constraint. Thinking about my remaining linear layout and table layout, I can define that the Low is below the current temp, but also aligned to the left hand edge, and then the actual temp is next to it. For the high temp, the temp is aligned to the right edge and then the H is next to that. This will give me an appropriate view:

The orange lines are the constraints. Since everything is constrained to either the parent or another object, positioning becomes easy. Unlike the relative layout, you can constraint just an edge – you don’t have to specify the relative position of the entire object. This means you can get very specific about the positioning and size of the widgets. When looking at my design in the design view, I see this:

Note the constraint links on every object. Let’s look at the equivalent code:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/background"
    tools:context="com.shellmonger.relativelayoutexample.MainActivity"
    tools:layout_editor_absoluteY="81dp"
    tools:layout_editor_absoluteX="0dp">

    <ImageView
        android:id="@+id/current_conditions_icon"
        android:layout_width="192dp"
        android:layout_height="192dp"
        android:contentDescription="@string/default_conditions"
        android:paddingBottom="24dp"
        android:paddingTop="24dp"
        android:src="@drawable/snow-icon-14"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_marginTop="0dp"
        android:layout_marginLeft="8dp"
        app:layout_constraintLeft_toLeftOf="parent"
        android:layout_marginRight="8dp"
        app:layout_constraintRight_toRightOf="parent" />

    <TextView
        android:id="@+id/current_conditions_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:fontFamily="sans-serif-light"
        android:paddingBottom="6dp"
        android:paddingTop="12dp"
        android:text="@string/default_conditions"
        android:textColor="@android:color/white"
        android:textSize="40sp"
        app:layout_constraintTop_toBottomOf="@+id/current_conditions_icon"
        android:layout_marginTop="0dp"
        android:layout_marginLeft="8dp"
        app:layout_constraintLeft_toLeftOf="parent"
        android:layout_marginRight="8dp"
        app:layout_constraintRight_toRightOf="parent" />

    <TextView
        android:id="@+id/current_location"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:fontFamily="sans-serif-light"
        android:paddingBottom="18dp"
        android:paddingTop="12dp"
        android:text="@string/default_location"
        android:textColor="@android:color/white"
        android:textSize="24sp"
        app:layout_constraintTop_toBottomOf="@+id/current_conditions_text"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent" />

    <TextView
        android:id="@+id/current_temperature"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="12dp"
        android:fontFamily="sans-serif-light"
        android:text="17°"
        android:textColor="@android:color/white"
        android:textSize="24sp"
        app:layout_constraintHorizontal_bias="0.522"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/guideline"
        app:layout_constraintTop_toBottomOf="@+id/current_location" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:fontFamily="sans-serif-light"
        android:text="@string/low_temp_label"
        android:textColor="@android:color/white"
        android:id="@+id/textView"
        app:layout_constraintHorizontal_chainStyle="packed"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/min_temperature"
        android:layout_marginTop="7dp"
        app:layout_constraintTop_toBottomOf="@+id/current_temperature" />

    <TextView
        android:id="@+id/min_temperature"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:fontFamily="sans-serif-light"
        android:text="15°"
        android:textColor="@android:color/white"
        app:layout_constraintRight_toLeftOf="@+id/textView2"
        app:layout_constraintLeft_toRightOf="@+id/textView"
        android:layout_marginTop="7dp"
        app:layout_constraintTop_toBottomOf="@+id/current_temperature" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:fontFamily="sans-serif-light"
        android:text="@string/high_temp_label"
        android:textColor="@android:color/white"
        app:layout_constraintRight_toLeftOf="@+id/max_temperature"
        app:layout_constraintLeft_toRightOf="@+id/min_temperature"
        android:layout_marginTop="7dp"
        app:layout_constraintTop_toBottomOf="@+id/current_temperature" />

    <TextView
        android:id="@+id/max_temperature"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:fontFamily="sans-serif-light"
        android:text="18°"
        android:textColor="@android:color/white"
        app:layout_constraintLeft_toRightOf="@+id/textView2"
        app:layout_constraintRight_toLeftOf="@+id/guideline"
        android:layout_marginRight="8dp"
        android:layout_marginTop="7dp"
        app:layout_constraintTop_toBottomOf="@+id/current_temperature"
        android:layout_marginEnd="8dp" />

    <TextView
        android:id="@+id/day1_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="12dp"
        android:drawableBottom="@drawable/snow-icon-14"
        android:fontFamily="sans-serif-light"
        android:text="@string/default_day1"
        android:textColor="@android:color/white"
        app:layout_constraintLeft_toLeftOf="@+id/guideline"
        app:layout_constraintRight_toLeftOf="@+id/day2_name"
        app:layout_constraintTop_toBottomOf="@+id/current_location" />

    <TextView
        android:id="@+id/day2_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="12dp"
        android:drawableBottom="@drawable/snow-icon-14"
        android:fontFamily="sans-serif-light"
        android:text="@string/default_day2"
        android:textColor="@android:color/white"
        app:layout_constraintLeft_toRightOf="@+id/day1_name"
        app:layout_constraintRight_toLeftOf="@+id/day3_name"
        app:layout_constraintTop_toBottomOf="@+id/current_location" />

    <TextView
        android:id="@+id/day3_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="12dp"
        android:drawableBottom="@drawable/snow-icon-14"
        android:fontFamily="sans-serif-light"
        android:text="@string/default_day3"
        android:textColor="@android:color/white"
        app:layout_constraintLeft_toRightOf="@+id/day2_name"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/current_location" />

    <ImageView
        android:id="@+id/info_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:contentDescription="@string/information"
        android:paddingEnd="6dp"
        android:paddingStart="6dp"
        android:src="@android:drawable/ic_menu_info_details"
        android:layout_marginLeft="8dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginBottom="8dp" />
    <TextView
        android:id="@+id/switcher"
        android:text="@string/threedots"
        android:paddingStart="6dp"
        android:paddingEnd="6dp"
        android:textColor="@android:color/white"
        android:textSize="24sp"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:ignore="RelativeOverlap"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginBottom="8dp"
        app:layout_constraintLeft_toRightOf="@+id/info_icon"
        android:layout_marginLeft="8dp"
        app:layout_constraintRight_toLeftOf="@+id/add_button"
        android:layout_marginRight="8dp" />

    <ImageView
        android:id="@+id/add_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentEnd="true"
        android:contentDescription="@string/add_location"
        android:paddingEnd="6dp"
        android:paddingStart="6dp"
        android:src="@android:drawable/ic_menu_add"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginBottom="8dp"
        android:layout_marginRight="8dp"
        app:layout_constraintRight_toRightOf="parent" />

    <android.support.constraint.Guideline
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/guideline"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.25"
        tools:layout_editor_absoluteY="0dp"
        tools:layout_editor_absoluteX="96dp" />
</android.support.constraint.ConstraintLayout>

I do not find this as intuitive. In fact, I struggle to understand where the widgets are actually going to be placed. This makes hand-designing the layout difficult, and understanding it later without the benefit of a designer preview practically impossible.

You can, if you like, convert a RelativeLayout to a ConstraintLayout. To convert the RelativeLayout:

  • Open up the Design tab for the layout.
  • Right-click the RelativeLayout in the Component Tree window.
  • Click Convert layout to ConstraintLayout.

Now go back and repair what you did, because it only converts the relative layout and it rarely gets it right in my experience. This is especially true when you try to convert linear layouts and table layouts. I’ve never got a conversion looking the same before and after the conversion. It may get the outer most RelativeLayout converted, or you may have to do some trivial clean-up. However, include a TableLayout and things seem to break.

Building a ConstraintLayout cannot really be done by hand. I like to draw my layouts by hand – with pen and paper. Drawing the zones for linear and table layouts, or the relative positioning for relative layouts is easy. Unfortunately, the ConstraintLayout does not seem to be as accommodating, which makes it harder to do specific design. Fortunately, there is a view editor for the constraint layout and great instructions on how to use it. If you have a graphic to work from, the ConstraintLayout and the designer are very capable tools to quickly produce a layout.

Wrap Up

I prefer hand-coded layouts as I understand them and can see when things go wrong just by inspecting the code. I can’t say the same for the ConstraintLayout. If you like drag-and-drop UI creation, then this is definitely the method you want. If you are like me and like hand-coded UI, then stick with the relative, linear and table layouts. There isn’t anything wrong with them!

One thought

  1. Pingback: Dew Drop - June 26, 2017 (#2507) - Morning Dew

Comments are closed.