Building a Scrum Poker App using MotionLayout
I think animations are one of the most complicated things to do in Android. Making a simple animation sometimes takes a lot of code, and it gets harder if you need to synchronise them.
Luckily, Android provides a few tools that enhance the user experience. One of the latest ones is MotionLayout
introduced in the Google I/O 2018. Even though it is still under development, the tool appears to be stable on beta 6.
So, I have decided to give it a go by writing a simple Scrum Poker application.
In case you don’t know what Scrum Poker is, here you can find the rules.
Summary
Basically the app will display 3 states:
- Initial: Showing all the cards
- Selected: Showing the selected card flipped down
- Revealed: The card is shown
The goal is to develop the app using as few lines of code as possible relying on the MotionLayout
XML description file to build and trigger the animations for us.
The finished project can be found on Github.
Let’s do it
If you plan to follow this post step by step, I encourage you to use Android Studio 4 (Currently on Preview) because a few design tools have been included. The Motion Editor, for example, allows you to navigate through the sequences, and help you preview what the layout will look like.
To start with, you need to use an Empty Activity project from the Android Studio Project Templates.
Once you have set up your Project, you just need to add the dependency in gradle.properties and you are good to go.
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta6'
Let’s set up a simple RecyclerView
with an Adapter to show the cards.
Now you might notice something strange, you are leaving the TextView
selected_card_title
outside the Card. What is this for?
MotionLayout
inherits from ConstraintLayout
, which works with a flat hierarchy view (there are no nested view groups). So if you want to animate or change the constraint of that view, you have to do it at root level.
Let’s use data binding for the cards, to reduce the boilerplate, and focus on the MotionLayout.
Now, let’s define the Domain Class and the Adapter to display these Cards.
Once the initial setup is done, you are ready to build your Scene.
A MotionScene is an XML resource file that contains all of the motion descriptions for the corresponding layout.
Source: https://developer.android.com/training/constraint-layout/motionlayout
Initial Sequence
This App will be used by tapping on the cards. Each of these taps leads to the next step until the last step returns to the initial one.
These “states” are built in the MotionScene
using sequences (ConstraintSet
) and Transition
.
A flat example of what you will build:
ConstraintSet
Specifies the positions and attributes of all of the views at one point in a motion sequence. This is done through the only possible child this Tag which is:
<Constraint>
Here you need to set the id of the view that you want to apply the changes to. Then, set of the properties to put the constraints.
This can be done by setting them on the parent tag or using a nested <Layout>
tag with the constraints, which is the recommended way to do it.
❌ Don’t
<Constraint android:id="@id/cards"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
✅ Do
<Constraint android:id="@id/cards">
<Layout
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</Constraint>
Why? This is important in order to use the property app:deriveConstraintsFrom
, which allows you to “inherit” the layout from the previous motion sequence, and reduce the number of lines needed to achieve the same results.
Transition
Specifies the beginning and end state of the motion sequence.
These elements are the drivers of the scene. Later on, you will see that these transitions can be triggered by user actions like clicks, but also can alter the sequence to synchronize the changes and produce a smoother transition.
The final result for the Initial Sequence
<ConstraintSet android:id="@+id/initial">
<Constraint android:id="@id/cards"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Constraint android:id="@id/selected_card">
<Layout
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="38dp"
android:layout_marginTop="38dp"
android:layout_marginEnd="38dp"
android:layout_marginBottom="38dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="2.5:3.5"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<PropertySet android:visibility="gone" />
<CustomAttribute
app:attributeName="cardBackgroundColor"
app:customColorValue="@color/cardBackColor" />
</Constraint>
<Constraint android:id="@id/selected_card_title">
<Layout
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@id/selected_card"
app:layout_constraintEnd_toEndOf="@id/selected_card"
app:layout_constraintStart_toStartOf="@id/selected_card"
app:layout_constraintTop_toTopOf="@id/selected_card" />
<PropertySet android:visibility="gone" />
</Constraint>
</ConstraintSet>
Selected Sequence
Now it is time to change the state when the user taps on one of the cards. But how is this new sequence supposed to look like? Let’s see… you want to:
- Hide the cards.
- Display the selected card, but we want to show it flipped.
- Hide the selected card number.
For this, you need to work a bit more. And as you can see depending on the changes you want to perform, the tag you need to use:
- For visibility (and alpha) you will need to use
<PropertySet>
. - For transformations included in the Android Animation API, you will need to use
<Transformation>
. - And for properties that are not in the Animation API. You will need to use
<CustomAttribute>
which uses reflection to update the property defined. This is quite useful when animating custom views, or in this case a material card.
<ConstraintSet
android:id="@+id/selected"
app:deriveConstraintsFrom="@id/initial">
<Constraint android:id="@id/cards">
<PropertySet android:alpha="0" />
</Constraint>
<Constraint android:id="@id/selected_card">
<PropertySet android:alpha="1" />
<Transform android:rotationY="180" />
<CustomAttribute
app:attributeName="cardBackgroundColor"
app:customColorValue="@color/cardBackColor" />
</Constraint>
<Constraint android:id="@id/selected_card_title">
<Transform android:rotationY="180" />
</Constraint>
</ConstraintSet><Transition
android:id="@+id/select_card"
app:constraintSetEnd="@id/selected"
app:constraintSetStart="@id/initial" />
At this point if you have Android Studio 4 running, you can use the Motion Editor to see the transition between the two sequences:
Improving the Transition
As you can see in the editor, the animation is a bit overwhelming, the cards are fading out while the “selected” card is spinning from the background.
So here are the KeyFrames to help you achieve a more organized transition.
<Transition
android:id="@+id/select_card"
app:constraintSetEnd="@id/selected"
app:constraintSetStart="@id/initial">
<KeyFrameSet>
<KeyAttribute
android:alpha="0"
app:framePosition="30"
app:motionTarget="@id/cards" />
<KeyAttribute
android:rotationY="0"
app:framePosition="30"
app:motionTarget="@id/selected_card" />
</KeyFrameSet>
</Transition>
What we are doing is rushing the alpha of the cards to 0 at 30% of the sequence. And then prevent the card from spinning until 30% of the sequence.
Final Sequence
I think we are ready to reveal the final MotionLayout
.
The last two transitions are quite similar to the first one, but I am adding an extra element to them:
<Transition
android:id="@+id/reveal_card"
app:constraintSetEnd="@id/revealed"
app:constraintSetStart="@id/selected">
<OnClick
app:clickAction="transitionToEnd"
app:targetId="@id/selected_card" />
</Transition><Transition
android:id="@+id/restart"
app:constraintSetEnd="@id/initial"
app:constraintSetStart="@id/revealed">
<OnClick
app:clickAction="transitionToEnd"
app:targetId="@id/selected_card" />
</Transition>
The <OnClick>
. This transition allows you to define a clickAction
to the element. This is quite useful in our case, since we want to move to the next sequence onClick
.
You probably want to enhance the transitions, in the same way we did it with the first one.
Synchronizing
I have added a KeyFrameSets at 50% in order to synchronize the appearing of the number at the same time the card is rotating.
<KeyFrameSet>
<KeyAttribute
android:alpha="0"
app:framePosition="50"
app:motionTarget="@id/selected_card_title" />
</KeyFrameSet>
Summarizing
We were aiming to build this application using as few lines of Kotlin code as possible relying on what MotionLayout
has to offer, a simple description file that is human readable.
In the end, it is easier to maintain a XML file than synchronize animations callbacks in your code.