Share

engineering

9min read

Android ConstraintLayout—The Guide to ConstraintHelpers

Android ConstraintLayout—The Guide to ConstraintHelpers

What are Android ConstraintLayout helpers

Android ConstraintLayout was seen at the Google I/O 2016 conference for the first time. It’s been 2.5 years since then and it kept evolving, adding new features. In the ConstraintLayout 1.1.0, a ConstraintHelper class became available for developers but as of now (the time of writing this article), no documentation has yet been published. Fortunately, that is not a big problem because Android is open sourced so we don’t have to rely on the documentation. In the following article, you’ll learn what the constraint helpers are and how to create a custom one. But first, let me describe the other class used by the ConstraintLayout.

Android ConstraintLayout—ConstraintWidget

Under the hoods, ConstraintLayout associates an object called ConstraintWidget with each of its “children”. You can see it being initialized during the LayoutParams creation. It acts as an abstraction. The linear system solver doesn’t operate on Android View objects, it uses ConstraintWidgets to calculate the positions of the views instead. Knowing this will be helpful in understanding the helpers.

image2

Android ConstraintLayout—ConstraintHelper

Constraint helpers are essentially Android Views, but they do not necessarily end up in the view hierarchy so it’s still flat. There are two reasons why they are Views:

  • they work nicely with LayoutInflater and that allows us to declare them in xml and later reference in the code
  • sometimes it’s useful to be able to actually draw them on the screen, for example Layer helper allows to draw a background behind the set of referenced views

The role of ConstraintHelper is to keep a reference to views and apply specific behaviors on them. There are already a couple of ConstraintHelper implementations like Group and Barrier. ConstraintLayout 2.0 introduces a concept of virtual layouts, for example, there is a helper that allows arranging a set of views in a linear fashion (like a linear layout). Other helpers can be used to draw something on the screen, like a circular reveal effects or custom animations. Helpers allow us to increase the code reusability.

If we take a look at the source of the ConstraintHelper class, we can see that there is a bit of documentation, but it’s annotated with @hide. We can read what ConstraintHelper is:

This class manages a set of referenced widgets. Helper objects can be created to act upon the set of referenced widgets. The difference between ConstraintHelper and ViewGroup is that multiple ConstraintHelper can reference the same widgets.

Let’s take a look at the simplest of the existing helpers—Group. As the doc says—it controls the visibility and the elevation of a set of referenced widgets, and we can use it like this:

<android.support.constraint.Group
    android:id="@+id/group"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="visible"
    app:constraint_referenced_ids="button4,button9" />

It may seem like a standard View but it has an interesting attribute: constraint_referenced_ids. We can see that it contains view ids but without @id/ or @+id/ prefixes.

Let’s get back to the ConstraintHelper class and look at its hidden doc again:

Widgets are referenced by being added to a comma separated list of ids

So that’s it—a comma separated list of ids. When we look at the ConstraintHelper init method, we can see the ids being parsed in setIds(String idList) method and if we follow the code, we’ll eventually get to the point where ids are stored in mIds[] array.

Here is a public API of ConstraintHelper class:

public abstract class ConstraintHelper extends View {
    public void onDraw(Canvas canvas) 
    public void validateParams() 
    public void updatePreLayout(ConstraintLayout container) 
    public void updatePostLayout(ConstraintLayout container) 
    public void updatePostMeasure(ConstraintLayout container)
    public void updatePostConstraints(ConstraintLayout constainer)
}

I think the method names are self-explanatory:) An important thing to notice here is that it actually extends Android View class.

Dissecting Group helper

To better understand how we can leverage the functionality of ConstraintHelper, let’s see how the Group helper works. As we can see, it’s pretty small class - it has less than 100 lines with comments.

It overrides 2 of the ConstraintHelper methods:

  • updatePreLayout - in this method it takes Group helper’s visibility and elevation values and applies them to all of its referenced views.

    @Override
    public void updatePreLayout(ConstraintLayout container) {
        int visibility = getVisibility(); // get visibility
        float elevation = 0;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
            elevation = getElevation(); // get elevation
        }
        for (int i = 0; i < mCount; i++) { // iterate over referenced ids
            int id = mIds[i];
            View view = container.getViewById(id); // get a child view from ConstraintLayout based on its id
            if (view != null) {
                view.setVisibility(visibility); // set the child visibility
                if (elevation > 0 && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
                    view.setElevation(elevation); // set the child elevation
                }
            }
        }
    }
  • updatePostLayout - in this method it sets both width and height values of the Group helper itself to 0 so it’s not visible on the screen.

    @Override
    public void updatePostLayout(ConstraintLayout container) {
        ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) getLayoutParams();
        // the Group itself will not be rendered as it's size is 0
        params.widget.setWidth(0);
        params.widget.setHeight(0);
    }

And that’s it. Pretty simple, but powerful!

Creating custom helper

Let’s see how we can create a custom ConstraintHelper class now. Recently, in the project I’m working on, I got a design where the views had their sizes expressed as percentage values of the screen;s width and height. For example, there was an ImageView with height equal to the 25% of screen height and width equal to 50% of the screen width. ConstraintLayout already handles percentage-based sizes but they are relative to the size of the ConstraintLayout itself. In my case, it would only work if the ConstraintLayout was the same size as the screen, which may not always be the case. If it was just this single view, I would probably end up creating a custom ImageView class that measures itself according to the design. But there were a lot of views with such percentage dimensions. This seems like a perfect candidate for a custom ConstraintHelper class!

Let’s start by defining our custom attributes:

<!-- attrs.xml file -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SizeHelper">
        <attr name="screenHeight_percent" format="float"/>
        <attr name="screenWidth_percent" format="float"/>
    </declare-styleable>
</resources>

screenHeight_percent and screenWidth_percent will be our attributes which will allow to specify the size of the views referenced by our helper. For example screenHeight_percent="0.5" means that all views referenced by SizeHelper should have width equal to 50% of screen width.

Now we can create our SizeHelper class and read these attributes values:

class SizeHelper @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ConstraintHelper(context, attrs, defStyleAttr) {

    // Screen dimensions
    private var screenHeight: Int = -1
    private var screenWidth: Int = -1

    // Attribute values
    private var layoutConstraintScreenHeightPercent = -1.0f
    private var layoutConstraintScreenWidthPercent = -1.0f

    init {
        attrs?.let { readAttributes(it) }
        setupScreenDimensions()
    }

    private fun readAttributes(attrs: AttributeSet) {
        val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.SizeHelper)
        layoutConstraintScreenHeightPercent = styledAttrs.getFloat(
                R.styleable.SizeHelper_screenHeight_percent, -1.0f)
        layoutConstraintScreenWidthPercent = styledAttrs.getFloat(
                R.styleable.SizeHelper_screenWidth_percent, -1.0f)

        styledAttrs.recycle()
    }

    private fun setupScreenDimensions() {
        screenWidth = resources.displayMetrics.widthPixels
        screenHeight = resources.displayMetrics.heightPixels
    }
}

This code looks like a normal custom view—it reads attributes and screen size and stores them in class properties. There is nothing special here yet. Now it’s time for the actual ConstraintHelper part. We’ll be overwriting referenced views dimensions so it looks like the method we should override is updatePostMeasure. Let’s take a look at it:

override fun updatePostMeasure(container: ConstraintLayout) {
    for (i in 0 until this.mCount) {
        val id = this.mIds[i]
        val child = container.getViewById(id)
        val widget = container.getViewWidget(child)

        val newHeight = screenHeight * layoutConstraintScreenHeightPercent
        widget.height = newHeight.toInt()
            
        val newWidth = screenWidth * layoutConstraintScreenWidthPercent
        widget.width = newWidth.toInt()        
    }
}

It iterates over all ids referenced by out helper. For each of the id it gets the Android View using container.getViewById() method from the container which is ConstraintLayout.

Then when we have a View, we can get its ConstraintWidget using container.getViewWidget(child) method. We can now change ConstraintWidget properties. We’re mostly interested in width and height properties. We calculate new values by multiplying screen dimensions by our custom attributes values and assign it to the widget.

And that’s all. We can now use our custom helper like this:

<ConstraintLayout>
    <!-- overwrite width and height of view1 -->
    <View
        android:id="@+id/view1"
        android:layout_width="0dp"
        android:layout_height="0dp"/>

    <!-- overwrite only height of view2 -->
    <View
        android:id="@+id/view2"
        android:layout_width="wrap_content"
        android:layout_height="0dp"/>

    <SizeHelper
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:screenHeight_percent="0.3"
        app:screenWidth_percent="0.5"
        app:constraint_referenced_ids="view1, view2"/>
</ConstraintLayout>

This will set view1 width to 30% of screen width and height to 50% of screen height. For view2 it’ll set only height to be 50% of screen height. Important thing to notice here is that we have set view’s width/height to 0dp if we want our helper to be able to overwrite it. We can of course specify only one of the dimensions as percent value and other to wrap_content or some specific value like 100dp.

Great thing about custom helpers is that they work with a layout preview and with a visual editor!

Check out see a full implementation of SizeHelper.

Conclusion

It may not be the most common case for helpers but using them allowed me to achieve the desired functionality really fast and I hope this post showed you how simple and powerful the ConstraintHelpers are. I think that animations are where they can really shine ! Can’t wait to see what helpers you will come up with. Happy coding!

Huge thanks to Nicolas Roard for proofreading this post.

Share

Michał

Senior Software Engineer

Did you enjoy the read?

If you have any questions, don’t hesitate to ask!

Did you enjoy the read?

If you have any questions, don’t hesitate to ask!