Refining an Onboarding Experience with MotionLayout

Tito Moyela
OT Design
Published in
6 min readJun 20, 2019

If you are not already familiar with MotionLayout, there are a number of articles by Nicolas Roard on the system which you can check out. They contain valuable information, as well as links to other valuable resources that will help you get started with it if you wish but, are also necessary to comprehend this article.

Why?

MotionLayout is a subclass of ConstraintLayout 2.0 library. This is great because it allows you to make use of all the nice features of the ConstraintLayout library. The most important of which for me, is the ability to constrain views to a layout, which allows the positions of views in a layout to be consistent across every screen size. When you take a look at the onboarding flow of Julio, it is easy to see why this is important. Below is a comparison of old, and new version of the first launch.

Old —> New

The animation works seemlessly regardless of presence of a notch, hardware navigation etc. or the lack thereof. Now, you do not need MotionLayout to achieve this but, it offers the cleanest solution by allowing you to constrain the position of the view to align with the splash screen background. If you would like to replicate this animation, make sure that your layout is stable.

Meaningful Animations

What is the point of a fancy splash screen transition? This is a fair question to ask at this point, especially if you are not a beta tester. This is the main screen of the app.

I wont get into detail about the whole concept of the app but, the cards are a core element. So, when we take a closer look at the interactive stage of the onboarding flow below, we start to see the important role MotionLayout played in allowing for a seamless, and meaningful animation.

A couple swipes later in the flow and we can see the card has made its way to the bottom of the screen to be used in yet another illustration.

Visual illustrations are very important when it comes to condensing information down so that the user does not feel overwhelmed, especially when they are visiting your app for the first time.

Julio as a game itself is quite difficult, and having this interactive step in the onboarding flow illustrated almost exactly how I envisioned when I first drafted this app is a huge plus. The natural flow, and illustration of the onboarding allows the user to intuitively absorb information, and the interactive step reinforces their learning.

WowoViewPager -> MotionLayout

An app like Julio presents a unique challenge, especially when you consider that it’s more of a game than an app. For the purpose of this article, I refer to it as an app since it uses a pretty much uses standard Architecture components with a Single Activity, and multiple Fragments.

Most games use an ad hoc onboarding experience. However, this can end up completely breaking the experience of the user, especially if they have a significant amount to learn. So as you can expect, I use a mixture of the quick-start and ad hoc methods. This article will only be covering the quick-start aspect, however. The “best onboarding experience” is less of a topic, and more of an everlasting debate that I will not be getting into in this article. For more information on this you can take a look at the iOS and Material Design HCI guidelines as a starting point.

Nonetheless, in my opinion users need certain things spelt out to them in an initial onboarding flow — the context in which they read the information is most consistent here i.e. any UX flows in your app, good or bad, have not yet altered their perception of your app. This is the best place to provide the user with an overview of your app so that they don’t feel flustered when they finally reach the first “functional” screen.

For the old onboarding flow, I used a library called WowoViewPager in which dimensions and positions are specified as pixel values. This as you can expect, presents its own challenges as far as tackling the age old problem of Android fragmentation.

My first solution to fixing this problem was to use the handy dpToPx converter method from the Resources class; passing dimension values declared in the dimens.xml file. However, this did not work consistently across all the views, as the animation of some views were not always driven by the viewpager. This caused some jittery animations, and certain views to look misaligned on some devices; breaking the user experience. Also, there are some additional issues when it comes to dealing with devices with a notch or hardware navigation buttons. Suffice it to say, this system that resulted from using this library has haunted me for a while as it led to a lot of elbow grease code.

The new onboarding implementation with MotionLayout is quite similar to the WowoViewPager implementation in that majority of the animations are driven by a ViewPager. For the new onboarding flow, I made use of an “empty” ViewPager that fills the full height of the layout, and simply created a custom view to extend the MotionLayout, then I attached an OnPageChangeListener, like so:

class OnboardingSceneView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0) :
MotionLayout(context, attrs, defStyleAttr),
ViewPager.OnPageChangeListener {

private val numPages = 16
override fun onPageScrollStateChanged(state: Int) {
}

override fun onPageScrolled(position: Int,
positionOffset: Float,
positionOffsetPixels: Int) {
progress = (position + positionOffset) / (numPages - 1)
}

override fun onPageSelected(position: Int) {
}
}

All of the positions and attributes for specific framePositions are then declared in the MotionScene. Each framePosition is related to the different positions of the ViewPager. Part IV of Nicolas’ article series provides enough insight into how this is done.

The versatility of MotionLayout, is superior to WowoViewPager, or any other libraries that I have come across, for that matter, as it allowed me to reuse certain views. Whereas, with WowoViewPager I had to create multiple instances of the same view, and create different variations of assets, because not all of its Animations were compatible with SVGs.

My Experience

In the end, I was able to get rid of 40% of the assets, and layout views. The interactive portion of the onboarding flow is a good example of this. I was able to use just one ImageFilterView to animate the image saturation, and its source into an Animated Vector Drawable, instead of three separate assets and views.

<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/julio"
android:layout_width="@dimen/julio_test_height"
android:layout_height="@dimen/julio_test_height"
android:scaleType="fitXY"
app:altSrc="@drawable/julio_avd"
android:src="@drawable/julio" />

There is one major bug I discovered with MotionLayout that caused the KeyAttributes and KeyPositions for a framePosition to be prematurely applied in another earlier framePosition. I am still trying to get down to the bottom of this. If I do, I will update this post.

Other than that, I found that it can be a bit daunting to keep track of all the KeyFrames but then again, there is really no better alternative. The states of views need to be declared somewhere. Moreover, there are an extraordinarily large number of KeyFrames in my use case.

For more simpler use cases, I think MotionLayout would be a great tool, and in most cases offers the most straightforward workflow. It helps that it facilitates separation of concerns by keeping your animations separate, and it is fully testable if your project requires it.

For my use case, keeping the KeyFrames in a separate xml layout and out of my code significantly improved readability of my codebase. For example, I am able to dynamically change the text shown to the user based on server responses, with just a few lines of code, which could not previously be done with WowoViewPager. Below is a snippet from my OnboardingActivity.kt file.

private fun initObservers(){
viewModel.mainText
.observe(this, Observer{this.updateMainTextView(it)})

...
}
override fun onPageSelected(position: Int) {
viewModel.getText(position)
...
}
...

I was not a fan of ConstraintLayout initially for many reasons, mostly due to its instability but, with the newest version most of my concerns have been addressed.

--

--