Search code examples
androidandroid-motionlayout

MotionLayout: multiple cumulative transitions


I'm using MotionLayout and am trying to apply a series of transitions (defined in MotionScene file). For example Transition1 changes position of View1, and then Transition2 subsequently changes position of View2. The issue I'm seeing is that Transition2 is causing View1 to revert to it's original layout. I'm triggering the transitions using calls to transitionToState() (I've also tried explicitly providing start/end state for transition using setTransition() and then calling transitionToEnd()).

I saw comment in https://www.raywenderlich.com/8883-motionlayout-tutorial-for-android-getting-started that

if you don’t provide an end constraint for a view, it will disappear. This happens because the library doesn’t know which constraints it should apply at the end of the animation

Is it necessary to also include info on View1 layout in ConstraintSet for Transition2?

There is a "Multi State" example in https://github.com/googlesamples/android-ConstraintLayoutExamples/blob/master/README.md but it looks like transitions included all start from single base state (rather than being "cumulative")


Solution

  • As @hoford said, the ConstraintSets are not merged each time they change. Apparently, this is also not possible without using reflection, although it would definitely be useful. Anyway, here is how to do it using reflection (through a few extension functions).

    I did create my own TransitionListener base class to provide support annotations and readable value names. The extension functions can be found here.

    What you can do is merge the two ConstraintSets and then mutate the endConstraintSet which can be obtained through TransitionListener#onTransitionChange:

    class AccumulativeTransitionListener: TransitionListener() {
    
            var didApplyConstraintSet = false
    
            override fun onTransitionChange(view: MotionLayout, @IdRes startConstraintSetId: Int, @IdRes endConstraintSetId: Int, progress: Float) {
                if (!didApplyConstraintSet) {
                    // Let's retrieve our ConstraintSets first
                    val startConstraintSet = view.getConstraintSet(startConstraintSetId)
                    val endConstraintSet = view.getConstraintSet(endConstraintSetId)
                    // Merge them (using an extension function)
                    val mergedConstraintSet = startConstraintSet + endConstraintSet
                    // Clear + Set them
                    endConstraintSet.setConstraints(mergedConstraintSet)
                    didApplyConstraintSet = true
                }
            }
    
            override fun onTransitionCompleted(view: MotionLayout, @IdRes constraintSetId: Int) {
                didApplyConstraintSet = false
            }
    
        }
    

    To not merge and apply the ConstraintSets each time TransitionListener#onTransitionChange is being called, there's a simple helper variable.

    Lastly, you have to attach the listener to your MotionLayout:

    val accumulativeListener = AccumulativeTransitionListener()
    motionLayout.setTransitionListener(accumulativeListener)
    

    Let me know if that works or if there are any bugs!