3

I am trying to get my views to animate using MotionLayout but want certain Constraints to animate in before others. I think this was the purpose of the motion:staggered property for Transition but I don't understand how that works and there are no examples of it successfully working anywhere. With updated versions of MotionLayout it seems we should have motion:motionStagger for individual Constraints but again I cannot seem to get it to stagger as desired. Only documentation I could find was here explaining the Enhanced Staggered API but I don't understand how to use it.

I have added my MotionLayout code below. For reference, I am using the 2.0.0-beta3' version of ConstraintLayout

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<Transition
    motion:constraintSetEnd="@+id/end"
    motion:constraintSetStart="@+id/start"
    motion:duration="300"
    motion:motionInterpolator="easeInOut"
    motion:staggered="0.4" />

<ConstraintSet android:id="@+id/start">
    <Constraint android:id="@id/translucentOverlay">
        <Layout
            android:layout_width="5dp"
            android:layout_height="5dp"
            motion:layout_constraintBottom_toBottomOf="@id/imageBorder"
            motion:layout_constraintEnd_toEndOf="@id/imageBorder"
            motion:layout_constraintStart_toStartOf="@id/imageBorder"
            motion:layout_constraintTop_toTopOf="@id/imageBorder" />
        <CustomAttribute
            motion:attributeName="alpha"
            motion:customFloatValue="0.0" />
        <Motion motion:motionStagger="2" />
    </Constraint>

    <Constraint android:id="@id/imageBorder">
        <Layout
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="8dp"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
        <CustomAttribute
            motion:attributeName="crossfade"
            motion:customFloatValue="0" />
        <Motion motion:motionStagger="2" />
    </Constraint>

    <Constraint android:id="@id/imageBackground">
        <Layout
            android:layout_width="32dp"
            android:layout_height="32dp"
            motion:layout_constraintBottom_toBottomOf="@id/imageBorder"
            motion:layout_constraintEnd_toEndOf="@id/imageBorder"
            motion:layout_constraintStart_toStartOf="@id/imageBorder"
            motion:layout_constraintTop_toTopOf="@id/imageBorder" />
        <Motion motion:motionStagger="2" />
    </Constraint>

    <Constraint android:id="@id/profileInitialText">
        <Layout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            motion:layout_constraintBottom_toBottomOf="@id/imageBackground"
            motion:layout_constraintEnd_toEndOf="@id/imageBackground"
            motion:layout_constraintStart_toStartOf="@id/imageBackground"
            motion:layout_constraintTop_toTopOf="@id/imageBackground" />
        <CustomAttribute
            motion:attributeName="alpha"
            motion:customFloatValue="1.0" />
        <Motion motion:motionStagger="2" />
    </Constraint>

    <Constraint android:id="@id/profileImage">
        <Layout
            android:layout_width="32dp"
            android:layout_height="32dp"
            motion:layout_constraintBottom_toBottomOf="@id/imageBackground"
            motion:layout_constraintEnd_toEndOf="@id/imageBackground"
            motion:layout_constraintStart_toStartOf="@id/imageBackground"
            motion:layout_constraintTop_toTopOf="@id/imageBackground" />
        <Motion motion:motionStagger="2" />
    </Constraint>

    <Constraint android:id="@id/name">
        <Layout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="128dp"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
        <CustomAttribute
            motion:attributeName="alpha"
            motion:customFloatValue="0.0" />
        <Motion motion:motionStagger="5" />
    </Constraint>

    <Constraint android:id="@id/description">
        <Layout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toBottomOf="@id/name" />
        <CustomAttribute
            motion:attributeName="alpha"
            motion:customFloatValue="0.0" />
        <Motion motion:motionStagger="5" />
    </Constraint>
</ConstraintSet>

<ConstraintSet android:id="@+id/end">
    <Constraint android:id="@id/translucentOverlay">
        <Layout
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
        <CustomAttribute
            motion:attributeName="alpha"
            motion:customFloatValue="1.0" />
        <Motion motion:motionStagger="2" />
    </Constraint>

    <Constraint android:id="@id/imageBorder">
        <Layout
            android:layout_width="88dp"
            android:layout_height="88dp"
            motion:layout_constraintBottom_toBottomOf="@id/imageBackground"
            motion:layout_constraintEnd_toEndOf="@id/imageBackground"
            motion:layout_constraintStart_toStartOf="@id/imageBackground"
            motion:layout_constraintTop_toTopOf="@id/imageBackground" />
        <CustomAttribute
            motion:attributeName="crossfade"
            motion:customFloatValue="1" />
        <Motion motion:motionStagger="2" />
    </Constraint>

    <Constraint android:id="@id/imageBackground">
        <Layout
            android:layout_width="70dp"
            android:layout_height="70dp"
            android:layout_marginTop="64dp"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
        <Motion motion:motionStagger="2" />
    </Constraint>

    <Constraint android:id="@id/profileInitialText">
        <Layout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            motion:layout_constraintBottom_toBottomOf="@id/imageBackground"
            motion:layout_constraintEnd_toEndOf="@id/imageBackground"
            motion:layout_constraintStart_toStartOf="@id/imageBackground"
            motion:layout_constraintTop_toTopOf="@id/imageBackground" />
        <Motion motion:motionStagger="2" />
        <CustomAttribute
            motion:attributeName="alpha"
            motion:customFloatValue="0.0" />
    </Constraint>
    <Constraint android:id="@id/profileImage">
        <Layout
            android:layout_width="70dp"
            android:layout_height="70dp"
            motion:layout_constraintBottom_toBottomOf="@id/imageBackground"
            motion:layout_constraintEnd_toEndOf="@id/imageBackground"
            motion:layout_constraintStart_toStartOf="@id/imageBackground"
            motion:layout_constraintTop_toTopOf="@id/imageBackground" />
        <Motion motion:motionStagger="2" />
    </Constraint>

    <Constraint android:id="@id/name">
        <Layout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toBottomOf="@id/profileImage" />
        <CustomAttribute
            motion:attributeName="alpha"
            motion:customFloatValue="1.0" />
        <Motion motion:motionStagger="5" />
    </Constraint>

    <Constraint android:id="@id/description">
        <Layout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="16dp"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toBottomOf="@id/name" />
        <CustomAttribute
            motion:attributeName="alpha"
            motion:customFloatValue="1.0" />
        <Motion motion:motionStagger="5" />
    </Constraint>
</ConstraintSet>

kjanderson2
  • 1,209
  • 12
  • 23

2 Answers2

8

Ok, so after messing around with this for a long time, a lot of trial and error, and studying the equations given in this release update, this is what I have come up with.

The linked article above gives us some somewhat confusing equations which are

Let The motionStagger value is S(Vi) The overall stagger value is stagger (from 0.0 - 1.0) The duration of the animation is duration The views animation duration = duration * (1 - stagger) The view starts animating at duration * (stagger - stagger * (S(Vi) - S(V0)) / (S(Vn) - S(V0)))

DETERMINING TRANSITION STAGGER VALUE:

To determine what you want the overall stagger to be, think about the number of views you are trying to stagger. The article I linked above states that the viewDuration = totalDuration*(1 - stagger) so we can rearrange this equation to become stagger = 1 - (viewDuration / totalDuration). In my case, since I want to have three different moments when views enter, I want to have my viewDuration / totalDuration to be about 1/3. To simplify the math, I have chosen to have my stagger as 0.6, making each viewDuration 400. So my transition code looks like the following

<Transition
    motion:constraintSetEnd="@+id/end"
    motion:constraintSetStart="@+id/start"
    motion:duration="1000"
    motion:motionInterpolator="easeInOut"
    motion:staggered="0.6" />

You'll notice that I increased the duration to 1000 to see the stagger more clearly (Once you figure out your stagger values, the duration here can be updated and the stagger should scale appropriately to fit within the time frame).

DETERMINING INDIVIDUAL VIEWS STAGGER VALUES:

So now we need to figure out what to put as the ? in <Motion motion:motionStagger="?" />

This is where the math gets very complicated. For each view that we are going to set a stagger on, they should be ordered by stagger value. The equation (modified to make slightly more readable than the article) we are given is:

animationStartTime = totalDuration * (stagger - stagger * ((staggerCurrentView - lowestStaggerValue)/(highestStaggerValue - lowestStaggerValue))

This is definitely a little complicated but I can break it down with my example.

So for my example we already talked about how I have three views that I want to stagger somewhat evenly (which is why we chose a stagger value of 0.6). I know based on the inverse structure of the equation below that the view with the highest motionStagger value will animate in first.

Let's say we have three views, an ImageView that I want to come in first, a TextView that I want to come in second, and a Button that I want to come in third. So I will assign the ImageView a motionStagger value of 3, the TextView a motionStagger value of 2, and the TextView a motionStagger value of 1. Let's do the calculations here:

Stagger value = 0.6
motionStaggerValues = 3 (for ImageView), 2 (for TextView), and 1(for Button) 
ImageView animationStartTime = 1000 * (0.6 - 0.6 * ((3-1)/(3-1)))
    = 1000 * (0.6 - 0.6 * (1)) = 1000 * 0 = 0

So the ImageView starts animating at 0 and animates for 400ms (as shown in the above section). Now let's calculate for the TextView

Stagger value = 0.6
motionStaggerValues = 3 (for ImageView), 2 (for TextView), and 1(for Button) 
TextView animationStartTime = 1000 * (0.6 - 0.6 * ((2-1)/(3-1)))
    = 1000 * (0.6 - 0.6 * (1/2)) = 1000 * 0.3 = 300

So the TextView starts animating at 300 and animates for 400ms.

Finally, let's calculate the start time for the Button:

Stagger value = 0.6
motionStaggerValues = 3 (for ImageView), 2 (for TextView), and 1(for Button) 
TextView animationStartTime = 1000 * (0.6 - 0.6 * ((1-1)/(3-1)))
    = 1000 * (0.6 - 0.6 * (0)) = 1000 * 0.6 = 600

So the Button starts animating at 600 and animates for 400ms.

These values can be shifted and staggered based on what you chose as your motionStagger values. I tried to make this as simple as possible for explanation sake but it can get very complicated depending on what you are trying to accomplish. Here is what the final code will look like for the example I outlined above.

<ConstraintSet android:id="@+id/start">
    <Constraint android:id="@id/imageView">
        ...
        <Motion motion:motionStagger="3" />
    </Constraint>

    <Constraint android:id="@id/textView">
        ...
        <Motion motion:motionStagger="2" />
    </Constraint>

    <Constraint android:id="@id/button">
        ...
        <Motion motion:motionStagger="1" />
    </Constraint>
</ConstraintSet>

Where you will need another parallel ConstraintSet for the end state.

kjanderson2
  • 1,209
  • 12
  • 23
1

The actual math of staggered can be a little confusing but in practice

Staggered

Each view that is animating is given a Stager value (app:motionStagger) By default the stagger value of a view is the Manhattan distance from the top most view in the list of views. You can manually set the value by the attribute

This assigns a floating point stagger value to each view tagged with motionStagger (Views not tagged are ignored). The view with the lowest floating point value (V0) is started first. The view with the highest floating point value (Vn) is started last.

  • For any view of stagger value S(Vi)
  • With the transition stagger value of TS (from 0.0 - 1.0)
  • The duration of the animation is duration
  • The views animation duration DS = duration * (1 -TS)
  • Call the stagger fraction SFi = (S(Vi) - S(V0)) / (S(Vn) - S(V0))
  • The view starts animating at: (duration-DS) * SFi

This math may be confusing. So a practical example If I have 3 views View1, View2, View3 which I set motionStagger to 2, 5 and 7, respectively and the animation duration as set to 5 seconds. When I set the transitions stagger to 0.4 the will progress as follows:

The animation duration is 3.0 sec = 5 * (1- 0.4)

View1 stagger fraction = 0 = (2-2)/(7-2)
View1 starts at 0.0 sec 
View1 end    at 3.0 sec (0.0 + 3.0)

View2 stagger fraction = 0.6 = (5-2)/(7-2)
View2 starts at 1.2 sec (5.0-3.0) * 0.6
View2 ends   at 4.2 sec 1.2 + 3.0

View3 stagger fraction = 1
View3 starts at 2.0 sec (5.0 - 3.0) * 1
View3 ends   at 5.0 sec 
hoford
  • 4,918
  • 2
  • 19
  • 19
  • This is very helpful for the math breakdown! I am trying to implement it and cannot seem to get the name/text to animate in after the background/image. I added my motionScene code to my question. What am I missing? – kjanderson2 Jan 03 '20 at 21:12
  • interestingly, I ran this test with the exact values on my views and the one with the motionStagger set to 7 entered first and the one with motionStagger set to 2 entered last. It seems maybe somewhere your math got a little wonky. Thanks for your help though. Breaking it down gave me a much better idea of how to proceed! – kjanderson2 Jan 03 '20 at 23:16
  • Yea I might have got it backward. This would be the behavior for -0.4. for 0.4 the stagger fraction would be 1- ... – hoford Jan 04 '20 at 03:43
  • The only big issue I see is your duration is 300. That means the entire animation will take 300ms. (about 10 frames) Each motion will last 180ms. Try 3000 (3 seconds) – hoford Jan 04 '20 at 04:25