In the previous part, we developed ActivableImageView programmatically using MotionLayout. This time, we will build an advanced progress bar with steps.

Getting started with the development

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. Reading the previous article will help you to understand this method, too.

StepProgressBarView

This time, we want to build a progress bar with steps. In contrast to MotionLayout created in XML, we will be able to create a dynamic number of steps instead of a static one. 

How to create MotionLayout programmatically in Android? Part 2

How to create advanced MotionLayout?

Compared to ActivableImageView from the previous article, this view is a lot more complex and requires a different approach.

  1. Create a custom view that inherits from MotionLayout class
  2. Create views for bars and each step
  3. Create initial constraints
  4. Create constraints for each step
  5. Create and add Transitions for each step
  6. Add controls and change the active image for the step

1. Custom view creation

Let’s think about what is needed to develop our view:

  • ActivableImageView for the image of a step. If it is the current step, then the image gets activated.
  • Inactive bar that is constrained from the first to the last step.
  • Active bar that is constrained from the first step to the current step to show the current progress. Initially, the end of the active bar is constrained by the first step, as we have no progress yet.
  • To make bars prettier, we can create anchors. We can use Space views and constraint them for each step to be centered horizontally for a given image and below the image. This will make the bar start and end directly below the image’s center, rather than the image’s start or end.

Step class will look like this:

data class Step(@DrawableRes val drawableRes: Int, @ColorRes val activeTint: Int, @ColorRes val inactiveTint: Int)

As in the previous article, there is a helper class that will allow us to create MotionLayout easier. We inherit StepStateMotionLayout

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

    init {
        context.theme.obtainStyledAttributes(
            attrs,
            R.styleable.StepProgressBarView,
            0,
            0,
        ).apply {
            try {
                activeTint = getColorOrThrow(R.styleable.StepProgressBarView_active_tint)
                inactiveTint = getColorOrThrow(R.styleable.StepProgressBarView_inactive_tint)
                animationDuration = getIntOrThrow(R.styleable.StepProgressBarView_duration)
            } finally {
                recycle()
            }
        }
    }

As we don’t need to build this view outside of XML, we don’t have a secondary constructor. 

To create steps, we have to initialize the view outside of XML.

  fun initialize(steps: List<Step>) {
        createViews(steps)
        createInitialConstraints()
        createTransitions()
    }

2. Create views and constraints for each step

We need to create images, anchors, active and inactive bars. Once we have created all steps, we set the first step as active.

private fun createViews(steps: List<Step>) {
        resetViews()
        steps.forEach {
            stepViews.add(createStepView(it))
        }
        stepViews.first().setActive(true)
        createBars()
    }

   private fun ConstraintLayout.createStepView(step: Step): StepView {
        val activableImageView = ActivableImageView(
            context,
            drawableRes = step.drawableRes,
            activeTint = step.activeTint,
            inactiveTint = step.inactiveTint,
            animationDuration = animationDuration
        )
        val activableImageViewId = generateViewId()
        activableImageView.id = activableImageViewId
        val layoutParamsWrapMarginMedium = LayoutParams(48.asDp(), 48.asDp())
        layoutParamsWrapMarginMedium.setMargins(16.asDp(), 16.asDp(), 16.asDp(), 16.asDp())
        addView(activableImageView, layoutParamsWrapMarginMedium)

        val anchor = Space(context)
        val anchorId = generateViewId()
        anchor.id = anchorId
        addView(anchor, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT))

        return StepView(activableImageViewId, anchorId, activableImageView)
    }

And then we can create bars. Note that order of view creation is important. We want the active bar to be above the inactive one. To do that, we have to create the inactive one first.

private fun createBars() {
        inactiveBarId = createBar(inactiveTint)
        activeBarId = createBar(activeTint)
    }

    private fun createBar(@ColorInt tint: Int): Int {
        val layoutParams = LayoutParams(LayoutParams.MATCH_CONSTRAINT, 4.asDp())
        layoutParams.setMargins(0.asDp(), 16.asDp(), 0.asDp(), 16.asDp())

        val imageView = ImageView(context)
        val imageViewId = generateViewId()
        imageView.id = imageViewId
        imageView.setImageResource(R.drawable.bar)
        ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(tint))
        addView(imageView, layoutParams)
        return imageViewId
    }

Notice that we save ids of views. This will come in handy for creating constraints later on.

3. Create initial constraints

Now we need to build constraints for the initial state.

override fun createInitialConstraints() = createConstraintSet().apply {
        createConstrainsForAllSteps()
        setConstraintsForAllAnchors()
        setConstraintsForInactiveBar()
        setConstraintsForActiveBar()
        applyTo(this@StepProgressBarView)
    }

Each image of our view can be chained horizontally together to balance the space between views. Besides that, we need to connect images to the top of the view.

private fun ConstraintSet.createConstrainsForAllSteps() {
        val viewIds = stepViews.map { it.imageViewId }.toIntArray()
        createHorizontalChain(ConstraintSet.PARENT_ID, ConstraintSet.LEFT, ConstraintSet.PARENT_ID, ConstraintSet.RIGHT, viewIds, null, ConstraintSet.CHAIN_SPREAD)
        viewIds.forEach {
            connect(it, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
        }
    }


Created anchors have to be positioned under each step, horizontally centered on the image. This means that we need to connect the top to the bottom of the anchor, the anchor’s start to the image’s start, and the same for the end.

private fun ConstraintSet.setConstraintsForAllAnchors() {
        stepViews.forEach {
            connect(it.anchorViewId, ConstraintSet.TOP, it.imageViewId, ConstraintSet.BOTTOM)
            connect(it.anchorViewId, ConstraintSet.START, it.imageViewId, ConstraintSet.START)
            connect(it.anchorViewId, ConstraintSet.END, it.imageViewId, ConstraintSet.END)
        }
    }

We use anchors to properly set constraints for both bars.

 private fun ConstraintSet.setConstraintsForInactiveBar() {
        val firstAnchor = stepViews.first().anchorViewId
        val lastAnchor = stepViews.last().anchorViewId
        connect(inactiveBarId, ConstraintSet.TOP, firstAnchor, ConstraintSet.BOTTOM)
        connect(inactiveBarId, ConstraintSet.START, firstAnchor, ConstraintSet.START)
        connect(inactiveBarId, ConstraintSet.END, lastAnchor, ConstraintSet.END)
        connect(inactiveBarId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
    }

    private fun ConstraintSet.setConstraintsForActiveBar() {
        val firstAnchor = stepViews.first().anchorViewId
        connect(activeBarId, ConstraintSet.TOP, inactiveBarId, ConstraintSet.TOP)
        connect(activeBarId, ConstraintSet.BOTTOM, inactiveBarId, ConstraintSet.BOTTOM)
        connect(activeBarId, ConstraintSet.START, firstAnchor, ConstraintSet.START)
        connect(activeBarId, ConstraintSet.END, firstAnchor, ConstraintSet.END)
    }

And then, in the previously shown function createInitialConstraints, we have to apply this constraintSet to the layout:

applyTo(this@StepProgressBarView)

4. Create constraints for each step

At this point, we have an initial step that can be successfully rendered. Our progress bar shows the first step as active. During the creation of our views, we have added info about every step and ids of bars.

private data class StepView(val imageViewId: Int, val anchorViewId: Int, private val activableImageView: ActivableImageView) {
        fun setActive(isActive: Boolean) = activableImageView.setActive(isActive)
    }

This will help us to create constraints for MotionLayout’s transitions.

override fun createConstraintsForSteps() = stepViews.map { 
        StepConstraintSet(generateViewId(), it.createConstraintsForStep()) 
    }

    private fun StepView.createConstraintsForStep() = createConstraintSet {
        connect(activeBarId, ConstraintSet.START, stepViews.first().anchorViewId, ConstraintSet.START)
        connect(activeBarId, ConstraintSet.END, anchorViewId, ConstraintSet.END)
    }

It is much easier to do than initial constraints. We have to set constraints only for things that will be changed during the transition. As we have ActivableImageView implemented, we don’t have to worry about animating each step’s image. So the only thing that needs to be animated is the active bar to show progress. It always has to be connected to the first anchor and to the current step’s anchor.

5. Create and add Transitions for each step

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

protected fun createTransitions() {
        stepConstraints.clear()
        stepConstraints.addAll(createConstraintsForSteps())
        val scene = MotionScene(this)
        // …
    }

It uses constraints created in the previous step:

protected abstract fun createConstraintsForSteps(): List<StepConstraintSet>

Then creates transitions between each step, which means pairing the start constraintSet to the end constraintSet. We don’t need every combination, but the MotionLayout needs to know how to get to each step. So we create transitions like 1 to 2, 2 to 3, 3 to 4, etc.

 private fun createConstraintsBetweenSteps(): MutableList<Pair<StepConstraintSet, StepConstraintSet>> {
        val allContraints = mutableListOf<Pair<StepConstraintSet, StepConstraintSet>>()
        stepConstraints.forEachIndexed { index, constraintsSet ->
            // No next step for last one
            if (index == stepConstraints.lastIndex) return@forEachIndexed
            allContraints.add(constraintsSet to stepConstraints[index + 1])
        }
        return allContraints
    }

   protected fun createTransitions() {
        // …
        var firstTransition: MotionScene.Transition? = null
        createConstraintsBetweenSteps().forEachIndexed { index, pair ->
            val transition = scene.createTransition(pair)
            scene.addTransition(transition)
            if (index == 0) {
                firstTransition = transition
            }
        }
        // …
    }

Then we need to set a scene and set the first transition.


   protected fun createTransitions() {
        // …
        setScene(scene)
        setTransition(firstTransition!!)
    }

6. Add controls and change the active image for the step

Then we have to implement what happens when a step is changed and some controls to our view. Our helper class (StepStateMotionLayout) has a function setStep implemented that is responsible for properly changing the current step and calling MotionLayout’s transitionToState to given constraintSetId.

  protected fun setStep(step: Int) {
        if (step == currentStep) return
        val oldStepIndex = currentStep - 1
        val newStepIndex = step - 1
        currentStep = step
        transitionToState(stepConstraints[newStepIndex].constraintSetId, animationDuration)
        onStepChanged(oldStepIndex, newStepIndex)
    }

In our view we have to implement onStepChanged which has to change the tint of step’s ActivableImageView:

override fun onStepChanged(oldIndex: Int, newIndex: Int) {
        stepViews[oldIndex].setActive(false)
        stepViews[newIndex].setActive(true)
    }

And we create some controls to allow users to change the currently selected step:

fun previousStep() {
        if (currentStep == 1) return
        setStep(currentStep - 1)
    }

    fun nextStep() {
        if (currentStep == stepViews.size) return
        setStep(currentStep + 1)
    }

    fun firstStep() {
        setStep(1)
    }

    fun lastStep() {
        setStep(stepViews.size)
    }

Usage

In XML, we create our view:

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

And then we can initialize steps for this view in Activity or Fragment:

private fun setupStepProgressBarExample() {
        val activeTint = R.color.active
        val inactiveTint = R.color.inactive
        with(binding.contentStepProgressBar) {
            stepProgressBar.initialize(
                listOf(
                    StepProgressBarView.Step(R.drawable.ic_add_shopping_cart_48px, activeTint, inactiveTint),
                    StepProgressBarView.Step(R.drawable.ic_shopping_cart_48px, activeTint, inactiveTint),
                    StepProgressBarView.Step(R.drawable.ic_payments_48px, activeTint, inactiveTint),
                    StepProgressBarView.Step(R.drawable.ic_local_shipping_48px, activeTint, inactiveTint),
                )
            )

            buttonPrevious.setOnClickListener {
                stepProgressBar.previousStep()
            }

            buttonNext.setOnClickListener {
                stepProgressBar.nextStep()
            }

            buttonFirst.setOnClickListener {
                stepProgressBar.firstStep()
            }

            buttonLast.setOnClickListener {
                stepProgressBar.lastStep()
            }
        }
    }

The final effect – animation

The final effect is a progress bar that changes the step’s image tint and alpha if it is activated and animates the bar to the currently selected step:

How to create MotionLayout programmatically in Android? Part 2

The MotionLayout above has dynamic steps. We can set any number of steps with different drawables. Creating this in XML wouldn’t be hard, but as the amount of possible combinations grows, we have to create more and more XML files to handle that. With our implementation, we don’t have to worry about it. Our bar will work regardless of the number of steps or drawables / tints used. 

Possible problems

Keep in mind that creating the MotionLayout programmatically is not an easy task, at least when you are not familiar with it. There is a lot of boilerplate code to do. You can try to inflate XML to make it a bit easier. But the main problematic part is creating correct constraints. You have to be sure which view connects to the other view. Programmatically created constraints are more error-prone than XML created. For example, let’s see setConstraintsForInactiveBar() constraint connected to parent’s bottom:

connect(inactiveBarId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)

Normally we wouldn’t need this, as our inactive bar is already connected to the bottom of imageView:

connect(inactiveBarId, ConstraintSet.TOP, firstAnchor, ConstraintSet.BOTTOM)

Without that, our MotionLayout is not working properly. It is not an obvious thing and is hard to debug. The inactive bar is properly positioned, but there is no active bar and the animation is bugged.

How to create MotionLayout programmatically in Android? Part 2

Some pieces of advice for your app

  • Create MotionLayout programmatically only if it is used in several places, and needs to be dynamic. The above problems can slow development down – keep that in mind when estimating tasks.
  • If there is a problem with rendering views correctly, check generated ids. It is possible to make some mistakes that will not throw an exception.
  • If views are created with proper ids, check for constraints and try to add some additional, potentially unneeded, ones.

Contact

Do you want to find out more about MotionLayout in Android?

Talk to us!

Use MotionLayout for efficient development in Android

The MotionLayout is a valuable tool for creating animations. The XML-created layout is static and is enough for most of our cases. But if we need a more dynamic approach, worry not! We can create everything programmatically to fulfill our requirements. Therefore, the UI of your Android application can introduce a more advanced experience for users! 

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