share

ENGINEERING

9min read

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

POLIDEA NEWSLETTER

Sign in and expect sharp insights, recommendations, ebooks and fascinating project stories delivered to your inbox

The controller of the personal data that you are about to provide in the above form will be Polidea sp. z o.o. with its registered office in Warsaw at ul. Przeskok 2, 00-032 Warsaw, KRS number: 0000330954, tel.: 0048 795 536 436, email: hello@polidea.com (“Polidea”). We will process your personal data based on our legitimate interest and/or your consent. Providing your personal data is not obligatory, but necessary for Polidea to respond to you in relation to your question and/or request. If you gave us consent to call you on the telephone, you may revoke the consent at any time by contacting Polidea via telephone or email. You can find detailed information about the processing of your personal data in relation to the above contact form, including your rights relating to the processing, HERE.

Data controller:

The controller of your personal data is Polidea sp. z o.o. with its registered office in Warsaw at ul. Przeskok 2, 00-032 Warsaw, KRS number: 0000330954, tel.: [0048795536436], email: [hello@polidea.com] (“Polidea”)

Purpose and legal bases for processing:

 

Used abbreviations:

GDPR – Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016
on the protection of natural persons with regard to the processing of personal data and on the free movement
of such data, and repealing Directive 95/46/EC (General Data Protection Regulation)

ARES – Polish Act on Rendering Electronic Services dated 18 July 2002

TL – Polish Telecommunications Law dated 16 July 2004

1)        sending to the given email address a newsletter including information on Polidea’s new projects, products, services, organised events and/or general insights from the mobile app business world |art. 6.1 a) GDPR, art. 10.2 ARES and art. 172.1 TL (upon your consent)

Personal data:name, email address

2)       statistical, analytical and reporting purposes |art. 6. 1 f) GDPR (based on legitimate interests pursued by Polidea, consisting in analysing the way our services are used and adjusting them to our clients’ needs, as well as developing new services)

Personal data:name, email address

Withdrawal of consent:

You may withdraw your consent to process your personal data at any time.

Withdrawal of the consent is possible solely in the scope of processing performed based on the consent. Polidea is authorised to process your personal data after you withdraw your consent if it has another legal basis for the processing, for the purposes covered by that legal basis.

Categories of recipients:

Your personal data may be shared with:

1)       authorised employees and/or contractors of Polidea

2)       persons or entities providing particular services to Polidea (accounting, legal, IT, marketing and advertising services) – in the scope required for those persons or entities to provide those services to Polidea

 

Retention period:

1)       For the purpose of sending newsletter to the given email address – for as long as the relevant consent is not withdrawn

2)       For statistical, analytical and reporting purposes – for as long as the relevant consent is not withdrawn

Your rights:

 

Used abbreviation:

GDPR – Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016
on the protection of natural persons with regard to the processing of personal data and on the free movement
of such data, and repealing Directive 95/46/EC (General Data Protection Regulation)

According to GDPR, you have the following rights relating to the processing of your personal data, exercised by contacting Polidea via [e-mail, phone].

1)       to access to your personal data (art. 15 GDPR) by requesting sharing and/or sending a copy of all your personal data processed by Polidea

2)       to request rectification of inaccurate personal data
(art. 16 GDPR) by indicating the data requiring rectification

3)       to request erasure of your persona data (art. 17 GDPR); Polidea has the rights to refuse erasing the personal data in specific circumstances provided by law

4)       to request restriction of processing of your personal data (art. 18 GDPR) by indicating the data which should be restricted

5)       to move your personal data (art. 20 GDPR) by requesting preparation and transfer by Polidea of the personal data that you provided to Polidea to you or another controller in a structured, commonly used machine-readable format

6)       to object to processing your personal data conducted based on art. 6.1 e) or f) GDPR, on grounds relating to your particular situation (art. 21 GDPR)

7)       to lodge a complaint with a supervisory authority,
in particular in the EU member state of your habitual residence, place of work or place of the alleged infringement if you consider that the processing
of personal data relating to you infringes the GDPR
(art. 77.1 GDPR)

No obligation to provide data:

Providing your personal data is not obligatory, but necessary for Polidea to provide you the newsletter service

Refusal to provide the above data will result in inability to receive the newsletter service.

Profiling

In the process of providing the newsletter service, we make decisions in an automated way, including profiling, based on the data you provide.

 

“Profiling” means automated processing of personal data consisting of the use of your personal data to evaluate certain personal aspects relating to you, in particular to analyze or predict aspects concerning your personal preferences and interests.

 

The automated decisions are taken based on the analysis of clicked and viewed content. They affect the targeting of specific newsletter content to selected users registered to receive the newsletter service, based on the anticipated interests of the recipient.