Share
January 24, 2019 | 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 Android 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.
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
ConstraintLayout 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 ConstraintLayout 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— Android ConstraintLayout 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 takesGroup
helper’svisibility
andelevation
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 theGroup
helper itself to0
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ł Zieliński
Senior Software Engineer