MotionLayout is a useful tool for creating animations on Android. XML-based motion layout support is limited and isn’t as dynamic as it could be. We can solve this by creating MotionLayout programmatically.

Before we start looking at the code

To read and possibly understand this article, you need to know the basics of MotionLayout and how to create a custom view to be used in XML. To keep it simple, the project is not created with production code in mind, meaning that resources are not extracted, the code lacks style management, etc.

ActivableImageView

In our example, we want to build a simple imageView that changes tint based on whether the imageView is set as active or not. It is a two-state animation, pretty simple to code in XML, so it will be easier to understand how it works programmatically.

How to create MotionLayout programmatically in an Android app?

To make MotionLayout functional, we need to do several things:

  1. Create a custom view that inherits from MotionLayout class
  2. Create and add views to the layout
  3. Create constraints (like in ConstraintLayout)
  4. Add constraints as “Transition” (information about the start state of view and desired state of view after the animation) to MotionLayout’s “Scene” (a mechanism that uses transitions to animate views)

Utilities

For starters, let’s check utilities that come in handy for MotionLayout development. 

To create constraints for views we need to create ConstraintSet, clone it, and then set properties. The function below makes it easier to do, with less boilerplate code:

fun ConstraintLayout.createConstraintSet(block: ConstraintSet.() -> Unit = {}): ConstraintSet {
    val set = ConstraintSet()
    set.clone(this)
    block(set)
    return set
}

It is common to center views in the parent view, so the following function changes it to a one-liner call:

fun ConstraintSet.centerInParent(viewId: Int){
    connect(viewId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
    connect(viewId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
    connect(viewId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
    connect(viewId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
}

Sometimes we need to know in which direction to animate:

fun MotionLayout.isStartState() = currentState == startState

fun MotionLayout.isEndState() = currentState == endState

And the last thing is converting dp to pixels, as it is essential for creating views programmatically. Note, that in typical applications, we should use dimensions from resource files.

fun Context.dpToPx(dp: Int): Int {
    return (dp * resources.displayMetrics.density).toInt()
}

fun Context.pxToDp(px: Int): Int {
    return (px / resources.displayMetrics.density).toInt()
}

1. Custom view creation

TwoStateMotionLayout is our helper class, which I will explain later on. TwoStateMotionLayout inherits from MotionLayout.

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

    init {
        // Obtain drawable, tint values and animation duration from attrs
        // We don’t need to call setupView, it will be called in TwoStateMotionLayout’s onFinishInflate which works for views created in XML
    }

   constructor(
        context: Context,
        @DrawableRes drawableRes: Int,
        @ColorRes activeTint: Int,
        @ColorRes inactiveTint: Int,
        animationDuration: Int,
        startAtTheEndTransition: Boolean = false,
    ) {
      	// Set drawable, tint values, and animation duration when view is created programmatically
	// We need to call setupView here, as the view is not created in XML
setupView()
    }

2. Create and add views to the layout

Let’s check what TwoStateMotionLayout (helper class) looks like:

protected fun setupView() {
        val constraintSets = createViewsAndConstraintSets()
        val scene = MotionScene(this)
        val transition = scene.createTransition(constraintSets)
        scene.addTransition(transition)
        setScene(scene)
        setTransition(transition)
        setTransitionDuration(animationDuration)
    }

    abstract fun ConstraintLayout.createViewsAndConstraintSets(): Pair<ConstraintSet, ConstraintSet>

As you see, TwoStateMotionLayout takes care of creating the transition. We need to implement the function createViewsAndConstraintSets which is responsible for creating views and their constraints.

We create imageView, generate and set ViewId, drawable, and add to parent (MotionLayout):

val imageView = ImageView(context)
        val imageViewId = generateViewId()
        imageView.id = imageViewId
        imageView.setImageResource(drawableRes)
        val layoutParams = LayoutParams(LayoutParams.MATCH_CONSTRAINT, LayoutParams.MATCH_CONSTRAINT)
        addView(imageView, layoutParams)

3. Create constraints

Then we have to build constraints using a utility function to simplify this:

  val startSet = createConstraintSet {
            setColorValue(imageViewId, "ColorFilter", inactiveTint)
            setAlpha(imageViewId, 0.3f)
            centerInParent(imageViewId)
        }

        val endSet = createConstraintSet {
            setColorValue(imageViewId, "ColorFilter", activeTint)
            setAlpha(imageViewId, 1f)
            centerInParent(imageViewId)
        }

As you can see, the image in the first state has inactiveTint and has 0.3 alpha.

The end state, the active one, changes those values to make the image fully visible and with activeTint.

The final function for steps 2 and 3:

 override fun ConstraintLayout.createViewsAndConstraintSets(): Pair<ConstraintSet, ConstraintSet> {
        val imageView = ImageView(context)
        val imageViewId = generateViewId()
        imageView.id = imageViewId
        val layoutParams = LayoutParams(LayoutParams.MATCH_CONSTRAINT, LayoutParams.MATCH_CONSTRAINT)
        imageView.setImageResource(drawableRes)
        addView(imageView, layoutParams)

        val startSet = createConstraintSet {
            setColorValue(imageViewId, "ColorFilter", inactiveTint)
            setAlpha(imageViewId, 0.3f)
            centerInParent(imageViewId)
        }

        val endSet = createConstraintSet {
            setColorValue(imageViewId, "ColorFilter", activeTint)
            setAlpha(imageViewId, 1f)
            centerInParent(imageViewId)
        }

        return startSet to endSet
    }

4. Add transition to the scene

TwoStateMotionLayout takes constraints created in the previous step, forms transition out of it, and adds to the scene, so we don’t have to implement this:

 private fun MotionScene.createTransition(sets: Pair<ConstraintSet, ConstraintSet>): MotionScene.Transition =
        TransitionBuilder.buildTransition(
            this,
            generateViewId(),
            generateViewId(),
            sets.first,
            generateViewId(),
            sets.second,
        )

Usage

ActivableImageView class has a method setActive which takes care of starting animation:

  fun setActive(isActive: Boolean) {
        if (isActive) {
            transitionToEnd()
        } else {
            transitionToStart()
        }
    }


Then we can add this view in XML:


   <com.applover.dynamicmotionlayoutbar.views.ActivableImageView
        android:id="@+id/imageViewProfile"
        android:layout_width="96dp"
        android:layout_height="96dp"
        android:layout_margin="16dp"
        android:src="@drawable/ic_add_shopping_cart_48px"
        app:active_tint="@color/active"
        app:inactive_tint="@color/inactive"
        app:duration="@integer/animation_medium"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

And then use in code, like:

private fun setupActivableImageExample() = with(binding.contentActivableImageView) {
        buttonActivate.setOnClickListener {
            activableImageView.setActive(true)
        }

        buttonDeactivate.setOnClickListener {
            activableImageView.setActive(false)
        }
    }

Contact

Do you want to find out more about MotionLayout?

Talk to us!

The final effect

The final effect is an ImageView that changes tint and alpha if it is activated:

We can also use this view in another MotionLayout, for example, to make a progress bar:

The MotionLayout above is created in XML and uses ActivableImageView. Now we don’t have to think about how to animate the imageView. We have to animate the bar only, which is a far easier task to do. We could animate imageView for such a simple case, but we would be forced to do it each time when we want such an effect in other motion layouts. ActivableImageView is a complex motion editor, and it can save us a lot of time. Moreover, creating a layout that corresponds well with your Android app can make the UI even more pleasant for users and enhance its success. 

In the part 2 of this article, we will take a step further. We will try to make a progress bar like the one above with a dynamic number of steps, which can’t be done in XML, read it here!

The full code is available on Applover’s github. Want to learn more about Android development at Applover? See more insights on our blog!