Using ConstraintLayout advanced features for Custom Dialogs on Android

Nikita Belokopytov
Quandoo
Published in
5 min readMay 20, 2019

--

Android tablets contain extensive screen real estate which can be tempting to use to enhance the user experience. In this article, I will highlight how we built a King-Size Dialog complete with two columns and custom-built buttons, at Quandoo.

The curious size of a DialogFragment

Since the introduction of API 11 and tablet support, Android has encountered an issue. Previously, the screen sizes ranged between 3” to 5”, meaning the API was built to discourage any form of experimentation. However, since the development of 7” and 10” tablets, there is now a need for them to be more flexible. So, DialogFragments appeared and it is now possible to supply a custom layout via the onCreateView method and use it as either a standalone Fragment or show it in a dialog format.

But why is it that a custom view provided as a layout looks completely different when shown as a dialog, rather than a fragment?

There are four main DialogFragment styles that we can provide to the setStyle method via onCreate.

Let’s start with STYLE_NORMAL and STYLE_NO_TITLE.

These two options provide our dialog with: a faded background, show and dismiss animations, the ability to dismiss the dialog when the background is tapped as opposed to the content, and some material design additions such as rounded corners. These are all positive features.

Unfortunately, it will also completely break and disregard our layout’s size and margins.

Take a look at this StackOverflow thread about the methods of defining the dialog’s size and margins. It ranges from code snippets and a lot of API digging here…

Activity activity = ...;
AlertDialog dialog = ...;

// retrieve display dimensions
Rect displayRectangle = new Rect();
Window window = activity.getWindow();
window.getDecorView().getWindowVisibleDisplayFrame(displayRectangle);

// inflate and adjust layout
LayoutInflater inflater = (LayoutInflater)activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View layout = inflater.inflate(R.layout.your_dialog_layout, null);
layout.setMinimumWidth((int)(displayRectangle.width() * 0.9f));
layout.setMinimumHeight((int)(displayRectangle.height() * 0.9f));

dialog.setView(layout);

… through to using the platform’s undocumented capabilities like:

Try wrapping your custom dialog layout into RelativeLayout instead of LinearLayout. That worked for me.

We weren’t completely sure about fighting the platform, so we also observed the two remaining styles: STYLE_NO_FRAME and STYLE_NO_INPUT. On paper these options give us complete control over the dialog size.

Success?

Unfortunately no. Instead they took everything else, and we are now solely responsible for all our visuals. So, the DialogFragment turned into a plain overlay.

Meaning, we would have to implement the faded background ourselves.

Our target design had custom buttons, custom margins and title fonts. But in general its behavior was very close to a normal dialog. As a result, we decided to go with the STYLE_NO_TITLE option.

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, R.style.Dialog)
}

DialogFragment disregards a setStyle call once the dialog’s view has been created. So we call it as early as possible.

To set the proper size of the dialog, we had to add the following code to the onResume method:

override fun onResume() {
super.onResume()
if (showsDialog) {
val width = resources.getDimensionPixelSize(R.dimen.landing_width)
val height = resources.getDimensionPixelSize(R.dimen.landing_height)
dialog.window?.setLayout(width, height)
}
}

Even though the size is static, it should adapt to the DPI of the screen. It’s better to use resources to specify dimensions for all DPI buckets and allow the system to select the best fit.

Margins and Guidelines

Our design seemed like a good fit for using a ConstraintLayout as it had a large amount of views, but minimal dynamic components. There were a couple of concerns though as DialogFragment would casually disregard the top-level layout margins. We had a choice: either wrap ConstraintLayout in a FrameLayout, or set margins for each element individually. The former would add another layout pass to our view, and could be misunderstood and broken by other developers with less context of the situation. The latter was tedious, but made the most sense, so initially we opted for that choice.

In the sample below you can see us diligently using layout_marginStart and layout_marginLeft on the title text view:

<TextView
android:id="@+id/landing_title_txt"
style="@style/Design.TextAppearance.Header1"
android:layout_marginStart="@dimen/spacing_large_xxx"
android:layout_marginLeft="@dimen/spacing_large_xxx"
android:layout_marginTop="@dimen/spacing_large_xxx"
android:text="@string/landing_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

After some internal reviews, however, we realised that there is an even better way to do it. Plus, it’s used elsewhere in the app by our very own Marcello Galhardo — margin Guidelines!

Guidelines are invisible ‘helper’ objects that are placed either vertically or horizontally, and serve as a reference point for constraints. They can be positioned at either an offset in DP or in a more dynamic case by specifying a percentage from the total width or height of its ConstraintLayout.

We have already used it as a divider for our columns, setting it at 40% for the layout’s width and as a border between the title and the content via its layout_constraintGuide_percent property:

<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_columns_vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.4" />

So, instead of specifying layout_margin on every view, why don’t we just add four more Guidelines? Each will substitute the left, right, bottom and top margins. We will also potentially need a divider between the content and the buttons:

<androidx.constraintlayout.widget.Guideline
android:id="@+id/landing_guideline_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="@dimen/spacing_large"/>

The aforementioned title view constrained to those would end up looking like:

<TextView
android:id="@+id/landing_title_txt"
style="@style/Design.TextAppearance.Header1"
android:text="@string/app_update_landing_title"
app:layout_constraintStart_toStartOf="@id/guideline_start"
app:layout_constraintTop_toTopOf="@id/guideline_top" />

It already looks cleaner, doesn’t it?

After testing out the effect, we noticed a slight problem with the buttons. It seemed as though our columns had a different height on the testing device in comparison to the designs. That was bad news — Guidelines are great when you need a border that won’t be affected by the change of the view’s content. However, they cannot be anchored to a particular view, so they cannot react to an increased height of one of the columns.

So, was there anything else that we could do that would not result in moving the buttons outside of the ConstraintLayout?

Who needs ViewGroups anyway?

There is a feature in ConstraintLayout that allows you to place a Guideline — that will work as a border of a ViewGroup that holds the desired views — with wrap_content set as one of its dimensions. It’s called a Barrier.

How do use it?

Since obviously a Barrier is not a real ViewGroup, we’ll have to supply the views that it has to stick to manually. It is done via as a comma-separated list in constraint_referenced_ids property. Once you do that, just select which side of the views do you need the Barrier to stick to, by specifying app:barrierDirection property as either start, bottom, end or top.

<androidx.constraintlayout.widget.Barrier
android:id="@+id/dialog_content_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="chat, divider, update_step_txt" />

Now we have a reference point for our buttons. Voila! No matter how high the columns are, the buttons will now stick to the barrier. Meaning, our task is complete!

Subscribe to the Quandoo blog to learn more about the people and the tech behind Quandoo. Or, apply for a position if you’d like to work with me and the many other talented developers who won’t stop until we change the dining experience for the better.

--

--

Nikita Belokopytov
Quandoo

Leading Mobile Engineering @ Autoscout24. Silver bullet denier and golden hammer sceptic.