An Android MotionLayout KeyCycle Tutorial

The previous chapters introduced and demonstrated the concepts of integrating animation into Android app user interfaces using the MotionLayout container combined with the features of the Android Studio MotionLayout editor. The chapter entitled “An Introduction to MotionLayout” briefly mentioned the cycle (KeyCycle) and time cycle (KeyTimeCycle) key frames and explained how these can be used to implement animations involving large numbers of repetitive state changes.

This chapter will cover cycle key frames in more detail before demonstrating how to make use of them in an example project using Android Studio and the Cycle Editor.

An Overview of Cycle Keyframes

Clearly, position keyframes can be used to add intermediate state changes into the animation timeline. While this works well for small numbers of state changes, it would be cumbersome to implement in larger quantities. To make a button shake 50 times when tapped to indicate that an error occurred, for example, would involve the manual creation of 100 position keyframes to perform small clockwise and anti-clockwise rotations. Similarly, to implement a bouncing effect on a view as it moves across the screen would be an equally time consuming task.

For situations where state changes need to be performed repetitively, MotionLayout includes the Cycle and Time Cycle keyframes. Both perform the same tasks, with the exception that KeyCycle frames are based on frame positions within an animation path, while KeyTimeCycles are time-based in cycles per second (Hz).

Using these KeyCycle frames, the animation timeline is essentially divided up into subsections (referred to as cycles), each containing one or more waves that define how a property of a view is to be modified throughout the timeline. To create a KeyCycle cycle, the following information is required:

  • target view – The id of the view on which the changes are to be made.
  • frame position – The position in the timeline at which the cycle is to start.
  • wave period – The number of waves to be included in the cycle.
  • attribute – The property of the view that is to be modified by the waves.
  • wave offset –  Offsets the cycle by the specified amount from the keyframe baseline.
  • wave shape – The shape of the wave (sin, cos, sawtooth, square, triangle, bounce or reverse sawtooth) Consider the following cycle key frame set:
<KeyFrameSet>
   <KeyCycle
       motion:framePosition="0"
       motion:motionTarget="@+id/button"
       motion:wavePeriod="1"
       motion:waveOffset="0dp"
       motion:waveShape="sin"
       android:translationY="50dp"/>

   <KeyCycle
       motion:framePosition="25"
       motion:motionTarget="@+id/button"
       motion:wavePeriod="1"
       motion:waveOffset="0dp"
       motion:waveShape="sin"
       android:translationY="50dp"/>

   <KeyCycle
       motion:framePosition="50"
       motion:motionTarget="@+id/button"
       motion:wavePeriod="1"
       motion:waveOffset="0dp"
       motion:waveShape="sin"
       android:translationY="50dp"/>

   <KeyCycle
       motion:framePosition="75"
       motion:motionTarget="@+id/button"
       motion:wavePeriod="1"
       motion:waveOffset="0dp"
       motion:waveShape="sin"
       android:translationY="50dp"/>

   <KeyCycle
       motion:framePosition="100"
       motion:motionTarget="@+id/button"
       motion:wavePeriod="1"
       motion:waveOffset="0dp"
       motion:waveShape="sin"
       android:translationY="50dp"/>
</KeyFrameSet>

The above key frame set divides the timeline into four equal cycles. Each cycle is configured to contain a sin wave shape which adjusts the translationY property of a button 50dp. When executed, this animation will cause the button to oscillate vertically multiple times within the specified range. This key frame set can be visualized as shown in Figure 45-1 where the four dots representing the key frame positions:

Figure 45-1

As currently implemented, each cycle contains a single wave. Suppose that instead of these evenly distributed waves, we need four waves within the last cycle. This can easily be achieved by increasing the wavePeriod property for the last KeyCycle element as follows:

.
.
    <KeyCycle
       motion:framePosition="75"
       motion:motionTarget="@+id/button"
       motion:wavePeriod="4"
       motion:waveOffset="0dp"
       motion:waveShape="sin"
       android:translationY="50dp"/>
.
.

After making this change, the frame set can be rendered in wave form as follows:

Figure 45-2

So far the examples in this chapter have been using sin waves. In fact, a number of different wave shapes are available when working with cycle key frames in MotionLayout. Figure 45-3, for example, illustrates the effect of changing the waveShape property for all the cycle key frames to the sawtooth wave shape:

Figure 45-3

In addition to sin and sawtooth, MotionLayout also supports triangle, square, bounce and reverseSawtooth wave shapes.

In the above examples, each cycle moves the button within the same range along the Y-axis. Suppose, however, that we need the second cycle to move the button a greater distance along the positive Y-axis. This involves making an adjustment to the waveOffset property of the second cycle as follows:

<KeyCycle 
        motion:framePosition="25"
        motion:target="@+id/button"
        motion:wavePeriod="1"
        motion:waveOffset="100dp"
        motion:waveShape="sin"
        android:translationY="50dp"/>

By making this change, we end up with a timeline that resembles Figure 45-4:

Figure 45-4

The movement of the button during the second cycle will now range between approximately 0 and 150dp on the Y-axis. If we still need the lower end of the range to match the other waves we can, of course, add 100dp to the translationY value:

<KeyCycle 
        motion:framePosition="25"
        motion:target="@+id/button"
        motion:wavePeriod="1"
        motion:waveOffset="100dp"
        motion:waveShape="sin"
        android:translationY="150dp"/>

This change now gives us the following wave form:

Figure 45-5

Using the Cycle Editor

Although not particularly complicated, it can take some time to get the exact cycle configuration you need by directly editing XML KeyCycle entries in the MotionScene file. In recognition of this fact, the Android engineers at Google have developed the Cycle Editor. This is a separate Java-based utility that is not yet part of Android Studio. The Cycle Editor allows you to visually design and test cycle key frame sets.

The Cycle Editor tool is provided in the form of a Java archive (jar) file which will require that the Java runtime be installed on your development system. Steps to install Java will vary depending on your operating system.

Once you have Java installed, the CycleEditor.jar file can be downloaded from the following URL:

https://github.com/googlesamples/android-ConstraintLayoutExamples/releases/download/1.0/CycleEditor.jar

Once downloaded, open a command-prompt or terminal window, change directory to the location of the jar file and run the following command:

java -jar CycleEditor.jar

Once the tool has loaded the screen shown in Figure 45-6 will appear:

Figure 45-6

The panel marked A in the above figure displays the XML for the keyframe set and can be edited either directly or using the controls in panel B. Panel C displays the rendering of the cycles in wave form. Unfortunately, this is not redrawn in real-time as changes are made. Instead, it must be refreshed by selecting the File -> Parse XML menu option. The panel marked D will show a live rendering of the cycle animations when the play button in panel B is clicked. The Examples menu provides access to a collection of example key frame sets which can be used both for learning purposes and as the basis for your own animations.

To demonstrate the use of both the Cycle Keyframe and the Cycle Editor, the remainder of this chapter will create a sample project which implements a KeyCycle-based animation effect.

Creating the KeyCycleDemo Project

Select the Start a new Android Studio project quick start option from the welcome screen and, within the resulting new project dialog, choose the Empty Activity template before clicking on the Next button.

Enter KeyCycleDemo into the Name field and specify com.ebookfrenzy.keycycledemo as the package name. Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo) and the Language menu to Java.

MotionLayout requires version 2.0.0 or later of the androidx.constraintlayout library. To check that the project is using this library, edit the Gradle Scripts -> build.gradle (Module: app) file and locate the ConstraintLayout dependency entry:

dependencies {
.
.
    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta6'
}

If the file does not reference at least version 2.0.0 of the library, edit the file so that it reads as above before clicking on the Sync Now link at the top of the editor window. Having modified the reference, select File -> Project Structure… followed by the Suggestions option to check if a more recent library update version than the one shown above is available. If a newer version is listed, click on the corresponding Update button followed by Apply to update the Gradle file.

With the layout editor in Design mode and the activity_main.xml file open, right-click on the ConstraintLayout entry and select the Convert to MotionLayout menu option:

Figure 45-7

After making the selection, click on the Convert button in the confirmation dialog.

Configuring the Start and End Constraints

The objective of this tutorial is to animate the movement of a button from one side of the device screen to the other including KeyCycle effects that cause the view to also move up and down along the Y-axis. The first step is to configure the start and end constraints.

With the activity_main.xml file loaded into the MotionLayout editor, select and delete the default TextView widget. Make sure the Motion Layout box (marked E in Figure 45-9 below) is selected before dragging and dropping a Button view from the palette so that it is centered vertically and positioned along the left-hand edge of the layout canvas:

Figure 45-8

To configure the constraints for the start point, select the start constraint set entry in the editor window (marked A in Figure 45-9):

Figure 45-9

Next, select the button entry within the ConstraintSet list (B). With the button entry still selected, click on the edit button (C) and select Create Constraint from the menu.

With the button still selected, use the Attributes tool window to set constraints on the top, left and bottom sides of the view as follows:

Figure 45-10

Select the end constraint set entry (marked D in Figure 45-9 above) and repeat the steps to create a new constraint, this time with constraints on the top, bottom and right-hand edges of the button:

Figure 45-11

Creating the Cycles

The next step is to use the Cycle Editor to generate the cycle key frames for the animation. With the Cycle Editor running, refer to the control panel shown in Figure 45-12 below:

Figure 45-12

Using the menu marked A, change the property to be modified from rotation to translationY.

Next, use the KeyCycle control (B) to select cycle 0 so that changes made elsewhere in the panel will be applied to  the first cycle. Move the Period slider to 1 and the translationY slider to 60 as shown in Figure 45-13 (refer to the XML panel to see the precise setting for the translationY value as you move the slider):

Figure 45-13

To see the changes so far in the graph, select the File -> Parse XML menu option. Using the values listed in Table 45-1, configure the settings for KeyFrames 1 through 4 (keeping in mind that you have already configured the settings in the KeyCycle 0 column):

 KeyCycle 0KeyCycle 1KeyCycle 2KeyCycle 3KeyCycle 4
Position0255075100
Period12321
translationY60601506060

Table 45-1

On completion of these changes, the key frame set XML should read as follows:

<KeyFrameSet>
   <KeyCycle
       motion:framePosition="0"
       motion:motionTarget="@+id/button"
       motion:wavePeriod="1"
       motion:waveOffset="0dp"
       motion:waveShape="sin"
       android:translationY="60dp"/>

   <KeyCycle
       motion:framePosition="25"
       motion:motionTarget="@+id/button"
       motion:wavePeriod="2"
       motion:waveOffset="0dp"
       motion:waveShape="sin"
       android:translationY="60dp"/>

   <KeyCycle
       motion:framePosition="50"
       motion:motionTarget="@+id/button"
       motion:wavePeriod="3"
       motion:waveOffset="0dp"
       motion:waveShape="sin"
       android:translationY="150dp"/>

   <KeyCycle
       motion:framePosition="75"
       motion:motionTarget="@+id/button"
       motion:wavePeriod="2"
       motion:waveOffset="0dp"
       motion:waveShape="sin"
       android:translationY="60dp"/>

   <KeyCycle
       motion:framePosition="100"
       motion:motionTarget="@+id/button"
       motion:wavePeriod="1"
       motion:waveOffset="0dp"
       motion:waveShape="sin"
       android:translationY="60dp"/>
</KeyFrameSet>

To view the graph with the new cycles, select the File -> Parse XML menu option to refresh the wave pattern which should now appear as illustrated in Figure 45-14:

Figure 45-14

Previewing the Animation

The cycle-based animation may now be previewed from within the Cycle Editor tool. Start the animation running by clicking on the play button (marked A in Figure 45-15). To combine the cycles with horizontal movement, change the second menu (B) from Stationary to West to East. Also, take some time to experiment with the time and linearity settings (C and D).

Figure 45-15

Adding the KeyFrameSet to the MotionScene

Within the Cycle Editor, highlight and copy the KeyCycle elements from the XML panel and paste them into the Transition section of the res -> xml -> activity_main_scene.xml file within Android Studio so that they are placed between the existing KeyFrameSet markers. Note also the increased duration setting and the addition of an OnClick handler to initiate the animation:

<Transition
    motion:constraintSetEnd="@+id/end"
    motion:constraintSetStart="@id/start"
    motion:duration="7000">
    <KeyFrameSet>
       <KeyCycle
           motion:framePosition="0"
           motion:motionTarget="@+id/button"
           motion:wavePeriod="1"
           motion:waveOffset="0dp"
           motion:waveShape="sin"
           android:translationY="60dp"/>

           <KeyCycle
               motion:framePosition="25"
.
.
   </KeyFrameSet>

   <OnClick motion:targetId="@id/button"
            motion:clickAction="toggle" />
.
.

Before proceeding check that each target property is correctly declared. At the time of writing, the Cycle Editor was using the outdated motion:target tag. For example:

motion:target="@+id/button

This will need to be changed for each of the five KeyCycle entires to read as follows:

motion:motionTarget="@+id/button

Once these changes have been made, compile and run the app on a device or emulator and click on the button to start and view the animation.

Note that the KeyCycle wave formation can also be viewed within the Android Studio MotionLayout editor as shown in Figure 45-16 below:

Figure 45-16

KeyCycle frame sets are not limited to one per animation. For example, add the following KeyFrameSet to the Transition section of the activity_main_scene.xml file to add some rotation effects to the button as it moves:

<KeyFrameSet>
    <KeyCycle
        motion:framePosition="0"
        motion:motionTarget="@+id/button"
        motion:wavePeriod="1"
        motion:waveOffset="0"
        motion:waveShape="sin"
        android:rotation="45"/>

    <KeyCycle
        motion:framePosition="25"
        motion:motionTarget="@+id/button"
        motion:wavePeriod="2"
        motion:waveOffset="0"
        motion:waveShape="sin"
        android:rotation="80"/>

    <KeyCycle
        motion:framePosition="50"
        motion:motionTarget="@+id/button"
        motion:wavePeriod="3"
        motion:waveOffset="0"
        motion:waveShape="sin"
        android:rotation="45"/>

    <KeyCycle
        motion:framePosition="75"
        motion:motionTarget="@+id/button"
        motion:wavePeriod="2"
        motion:waveOffset="0"
        motion:waveShape="sin"
        android:rotation="80"/>

    <KeyCycle
        motion:framePosition="100"
        motion:motionTarget="@+id/button"
        motion:wavePeriod="1"
        motion:waveOffset="0"
        motion:waveShape="sin"
        android:rotation="45"/>
</KeyFrameSet>

Summary

Cycle key frames provide a useful way to build frame animations that involve potentially large numbers of state changes that match wave patterns. As outlined in this chapter, the process of generating these cycle key frames can be eased significantly by making use of the Cycle Editor application.

An Android MotionLayout Editor Tutorial

Now that the basics of MotionLayout have been covered, this chapter will provide an opportunity to try out MotionLayout in an example project. In addition to continuing to explore the main features of MotionLayout, this chapter will also introduce the MotionLayout editor and explore how it can be used to visually construct and modify MotionLayout animations.

The project created in this chapter will make use of start and end ConstraintSets, gesture handlers and Attribute and Position Keyframes.

Creating the MotionLayoutDemo Project

Select the Start a new Android Studio project quick start option from the welcome screen and, within the resulting new project dialog, choose the Empty Activity template before clicking on the Next button.

Enter MotionLayoutDemo into the Name field and specify com.ebookfrenzy.motionlayoutdemo as the package name. Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo) and the Language menu to Java.

MotionLayout requires version 2.0.0 or later of the androidx.constraintlayout library. To check that the project is using this library, edit the Gradle Scripts -> build.gradle (Module: app) file and locate the ConstraintLayout dependency entry:

dependencies {
.
.
    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta6'
.
.
}

If the file does not reference at least version 2.0.0 of the library, edit the file so that it reads as above before clicking on the Sync Now link at the top of the editor window. Having modified the reference, select File -> Project Structure… followed by the Suggestions option to check if a more recent library update version than the one shown above is available. If a newer version is listed, click on the corresponding Update button followed by Apply to update the Gradle file.

ConstraintLayout to MotionLayout Conversion

As usual, Android Studio will have placed a ConstraintLayout container as the parent view within the activity_ main.xml layout file. The next step is to convert this container to a MotionLayout instance. Within the Component Tree, right-click on the ConstraintLayout entry and select the Convert to MotionLayout menu option:

Figure 44-1

After making the selection, click on the Convert button in the confirmation dialog. Once conversion is complete, the MotionLayout editor will appear within the main Android Studio window as illustrated in Figure 44-2:

Figure 44-2

As part of the conversion process, Android Studio will also have created a new folder named res -> xml and placed within it a MotionLayout scene file named activity_main_scene.xml:

Figure 44-3

This file consists of a top level MotionScene element containing the ConstraintSet and Transition entries that will define the animations to be performed within the main layout. By default, the file will contain empty elements for the start and end constraint sets and an initial transition:

<?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="1000">
       <KeyFrameSet>
       </KeyFrameSet>
    </Transition>

    <ConstraintSet android:id="@+id/start">
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
    </ConstraintSet>
</MotionScene>

Any changes made within the MotionLayout editor will be stored within this file. Similarly, this file may be edited directly to implement and modify animation settings outside of the MotionLayout editor. In this tutorial, the animations will be implemented primarily using the MotionLayout editor interface. At each stage, however, we will take time to review how these changes are reflected in the underlying MotionScene file. As we progress through the chapter it will become clear that the MotionScene XML syntax is actually quite simple and easy to learn.

The first phase of this tutorial will demonstrate the use of MotionLayout to animate a Button object, including motion (including following a path), rotation and size scaling.

Configuring Start and End Constraints

With the activity_main.xml file loaded into the MotionLayout editor, make sure that the Motion Layout box (marked E in Figure 44-5 below) is selected, then delete the default TextView before dragging and dropping a Button view from the palette to the top left-hand corner of the layout canvas as shown in Figure 44-4:

Figure 44-4

With the button selected, use the Attributes tool window to change the id to myButton.

As outlined in the previous chapter, MotionLayout animation is primarily a case of specifying how a view transitions between two states. The first step in implementing animation, therefore, is to specify the constraints that define these states. For this example, the start point will be the top left-hand corner of the layout view. To configure these constraints, select the start constraint set entry in the editor window (marked A in Figure 44-5):

Figure 44-5

When the start box is selected, all constraint and layout changes will be made to the start point constraint set. To return to the standard constraints and properties for the entire layout, click on the Motion Layout box (E).

Next, select the myButton entry within the ConstraintSet list (B). Note that the Source column shows that the button positioned based on constraints within the layout file. Instead, we want the button to be positioned based on the start constraint set. With the myButton entry still selected, click on the Edit button (C) and select Create Constraint from the menu, after which the button entry will indicate that the view is to be positioned based on the start constraint set:

Figure 44-6

The start constraint set will need to position the button at the top of the layout with an 8dp offset and centered horizontally. With myButton still selected, use the Attributes tool window to set constraints on the top, left and right sides of the view as follows:

Figure 44-7

Select the end constraint set entry (marked D in Figure 44-5 above) and repeat the steps to create a new constraint, this time placing the button in the horizontal center of the layout but with an 8p offset from the bottom edge of the layout:

Figure 44-8

With the start and end constraints configured, open the activity_main_scene.xml file and note that the constraints have been added to the file:

<?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">
.
.
    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/myButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            motion:layout_constraintTop_toTopOf="parent"
            android:layout_marginTop="8dp"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintEnd_toEndOf="parent" />
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/myButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            motion:layout_editor_absoluteY="6dp"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintBottom_toBottomOf="parent"
            android:layout_marginBottom="8dp" />
    </ConstraintSet>
</MotionScene>

Note also that the Transition element has already been preconfigured to animate the transition between the start and end points over a period of 1000 milliseconds. Although we have yet to add an action to initiate the transition it is still possible to preview the animation from within the MotionLayout editor.

Previewing the MotionLayout Animation

To preview the animation without having to build and run the app, select the transition arrow within the MotionLayout editor marked A in Figure 44-9 below. This will display the animation timeline panel (marked B):

Figure 44-9

To test the animation, click on the slider (C) and drag it along the timeline. As the slider moves the button in the layout canvas will move along the dotted path line (D). Use the toolbar button (E) to perform a full animation, to repeat the animation continuously at different speeds (either forwards, backwards, or toggling back and forth).

Adding an OnClick Gesture

Although a simple MotionLayout animation transition has been created, we still need a way to start the animation from within the running app. This can be achieved by assigning either a click or swipe handler. For this example, we will configure the animation to start when the button is clicked by the user. Within the MotionLayout editor, begin by pausing the timeline animation if it is currently running on a loop setting. Next, select the Transition arrow (marked A in Figure 44-9 above), locate the OnClick attribute section in the Attributes tool window and click on the + button indicated by the arrow in Figure 44-10 below:

Figure 44-10

An empty row will appear in the OnClick panel for the first property. For the property name, enter targetId and for the value field enter the id of the button (@id/myButton). Click the + button a second time, this time entering clickAction into the property name field. In the value field, click the down arrow to display a menu of valid options:

Figure 44-11

For this example, select the toggle action. This will cause the view to animate to the opposite position when it is clicked. Once these settings have been entered, they should match those shown in Figure 44-12:

Figure 44-12

Once again, open the activity_main_scene.xml file and review the OnClick property defined within the Transition entry:

.
.
   <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="1000">
       <KeyFrameSet>
       </KeyFrameSet>
        <OnClick motion:targetId="@id/myButton"
            motion:clickAction="toggle" />
    </Transition>
.
.

Compile and run the app on a device or emulator and confirm that clicking on the button causes it to transition back and forth between the start and end points as defined in the MotionScene file.

Adding an Attribute Keyframe to the Transition

So far the example project is only animating the motion of the button view from one location on the screen to another. Attribute keyframes (KeyAttribute) provide a way to specify points within the transition timeline at which other attribute changes are to have taken effect. A KeyAttribute could, for example, be defined such that the view must have increased in size by 50% by the time the view has moved 30% of the way through the timeline. For this example, we will add a rotation effect positioned at the mid-point of the animation.

Begin by opening the activity_main.xml file in the MotionLayout Editor, selecting the transition connector arrow to display the timeline, then click on the button highlighted in Figure 44-13:

Figure 44-13

From the menu, select the KeyAttribute option:

Figure 44-14

Once selected, the dialog shown in Figure 44-15 will appear. Within the dialog, make sure the ID option is selected and that myButton is referenced. In the position field, enter 50 (this is specified as a percentage where 0 is the start point and 100 the end). Finally select the rotation entry from the Attribute drop-down menu before clicking on the Add button:

Figure 44-15

Once the KeyAttribute has been added, a row will appear within the timeline for the attribute. Click on the row to highlight it, then click on the disclosure arrow in the far left edge of the row to unfold the attribute transition graph. Note that a small diamond marker appears in the timeline (as indicated in Figure 44-16 below) indicating the location of the key. The graph indicates the linearity of the effect. In this case, the button will rotate steadily up to the specified number of degrees, reaching maximum rotation at the location of the keyframe. The button will then rotate back to 0 degrees by the time it reaches the end point:

Figure 44-16

To change the properties of a KeyAttribute, select it in the timeline and then refer to the Attributes tool window. Within the KeyAttribute panel, change the rotation property to 360 degrees:

Figure 44-17

Check that the attribute works by moving the slider back and forth and watching the button rotate as it traverses the animation path in the layout canvas. Refer to the activity_main_scene.xml file which should now as follows:

.
.
   <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="1000">
       <KeyFrameSet>
           <KeyAttribute
               motion:motionTarget="@+id/myButton"
               motion:framePosition="50"
               android:rotation="360" />
       </KeyFrameSet>
        <OnClick />
    </Transition>
.
.

Test the animation, either using the transition slider, or by compiling and running the app and verify that the button now rotates during the animation.

Adding a CustomAttribute to a Transition

The KeyAttribute property is limited to built-in effects such as resizing and rotation. Additional changes are also possible by declaring CustomAttributes. Unlike KeyAttributes, which are stored in the Transition element, CustomAttributes are located in the start and end constraint sets. As such, these attributes can only be declared to take effect at start and end points (in other words you cannot specify an attribute keyframe at a position partway through a transition timeline).

For this example, we will configure the button to gradually change color from red to green. Begin by selecting the start box marked A in Figure 44-18 followed by the myButton view constraint set (B):

Figure 44-18

Referring to the Attributes tool window, click on the + button in the CustomAttributes section as highlighted below:

Figure 44-19

In the resulting dialog (Figure 44-20) change the type of the attribute to Color and enter backgroundColor into the Attribute Name field. Finally, set the value to #F80A1F:

Figure 44-20

Click on OK to commit the changes, then select the end constraint set (marked C in Figure 44-18 above) and repeat the steps to add a custom attribute, this time specifying #33CC33 as the RGB value for the color.

Using either the timeline slider, or by running the app, make sure that the button changes color during the animation.

The addition of these CustomAttributes will be reflected in the activity_main_scene.xml file as follows:

.
.
   <ConstraintSet android:id="@+id/start">
        <Constraint
.
.
            <CustomAttribute
                motion:attributeName="backgroundColor"
                motion:customColorValue="#F80A1F" />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
.
.
            <CustomAttribute
                motion:attributeName="backgroundColor"
                motion:customColorValue="#33CC33" />
        </Constraint>
    </ConstraintSet>
.
.

Adding Position Keyframes

The final task for this tutorial is to add two position keyframes (KeyPosition) to the animation path to introduce some lateral movement in to the animation. With the transition timeline visible in the MotionLayout editor, click on the button to create a keyframe as highlighed in Figure 44-13 above and select the KeyPosition option from the menu as shown in Figure 44-21 below:

Figure 44-21

In the resulting dialog, set the properties as illustrated in Figure 44-22:

Figure 44-22

Click on the Add button to commit the change, then repeat the above steps to add a second position keyframe configured as follows:

  • Position: 75
  • Type: parentRelative
  • PercentX: 0.85
  • PercentY: 0.75

On completion of these changes, the following keyframe entries will have been added to the transition element in the activity_main_scene.xml file:

<KeyFrameSet>
.
.
   <KeyPosition
       motion:motionTarget="@+id/myButton"
       motion:framePosition="25"
       motion:keyPositionType="parentRelative"
       motion:percentX="0.15"
       motion:percentY="0.25" />
   <KeyPosition
       motion:motionTarget="@+id/myButton"
       motion:framePosition="75"
       motion:keyPositionType="parentRelative"
       motion:percentX="0.85"
       motion:percentY="0.75" />
</KeyFrameSet>
.
.

Test the app one last time and verify that the button now follows the path shown below while still rotating and changing color:

Figure 44-23

Summary

This chapter has introduced the MotionLayout editor built into Android Studio and explored how it can be used to add animation to the user interface of an Android app without having to manually write XML declarations. Examples covered in this chapter included the conversion of a ConstraintLayout container to MotionLayout, the creation of start and end constraint sets and transitions in  the MotionScene file and the addition of an OnClick handler. The use of the animation previewer, custom attributes  and postion key frames were also covered.

An Introduction to Android MotionLayout

The MotionLayout class provides an easy way to add animation effects to the views of a user interface layout. This chapter will begin by providing an overview of MotionLayout and introduce the concepts of MotionScenes, Transitions and Keyframes. Once these basics have been covered, the next two chapters (entitled “An Android MotionLayout Editor Tutorial” and “A MotionLayout KeyCycle Tutorial”) will provide additional detail and examples of MotionLayout animation in action through the creation of example projects.

An Overview of MotionLayout

MotionLayout is a layout container, the primary purpose of which is to animate the transition of views within a layout from one state to another. MotionLayout could, for example, animate the motion of an ImageView instance from the top left-hand corner of the screen to the bottom right-hand corner over a specified period of time. In addition to the position of a view, other attribute changes may also be animated, such as the color, size or rotation angle. These state changes can also be interpolated (such that a view moves, rotates and changes size throughout the animation).

The motion of a view using MotionLayout may be performed in a straight line between two points, or implemented to follow a path comprising intermediate points located at different positions between the start and end points. MotionLayout also supports the use of touches and swipes to initiate and control animation.

MotionLayout animations are declared entirely in XML and do not typically require that any code be written. These XML declarations may be implemented manually in the Android Studio code editor, visually using the MotionLayout editor, or using a combination of both approaches.

MotionLayout

When implementing animation, the ConstraintLayout container typically used in a user interface must first be converted to a MotionLayout instance (a task which can be achieved by right-clicking on the ConstraintLayout in the layout editor and selecting the Convert to MotionLayout menu option). MotionLayout also requires at least version 2.0.0 of the ConstraintLayout library.

Unsurprisingly since it is a subclass of ConstraintLayout, MotionLayout supports all of the layout features of the ConstraintLayout. A user interface layout can, therefore, be designed in exactly the same way when using MotionLayout for any views that do not require animation.

For views that are to be animated, two ConstraintSets are declared defining the appearance and location of the view at the start and end of the animation. A transition declaration defines key frames to apply additional effects to the target view between these start and end states, together with click and swipe handlers used to start and control the animation. Both the start and end ConstraintSets and the transitions are declared within a MotionScene XML file.

MotionScene

As we have seen in earlier chapters, an XML layout file contains the information necessary to configure the appearance and layout behavior of the static views presented to the user, and this is still the case when using MotionLayout. For non-static views (in other words the views that will be animated) those views are still declared within the layout file, but the start, end and transition declarations related to those views are stored in a separate XML file referred to as the MotionScene file (so called because all of the declarations are defined within a MotionScene element). This file is imported into the layout XML file and contains the start and end ConstraintSets and Transition declarations (a single file can contain multiple ConstraintSet pairs and Transition declarations allowing different animations to be targeted to specific views within the user interface layout). The following listing shows a template for a MotionScene file:

<?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="1000">
       <KeyFrameSet>
       </KeyFrameSet>
    </Transition>

    <ConstraintSet android:id="@+id/start">
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
    </ConstraintSet>
</MotionScene>

In the above XML, ConstraintSets named start and end (though any name can be used) have been declared which, at this point, are yet to contain any constraint elements. The Transition element defines that these ConstraintSets represent the animation start and end points and contains an empty KeyFrameSet element ready to be populated with additional animation key frame entries. The Transition element also includes a millisecond duration property to control the running time of the animation.

ConstraintSets do not have to imply motion of a view. It is possible, for example, to have the start and end sets declare the same location on the screen, and then use the transition to animate other property changes such as scale and rotation angle.

Configuring ConstraintSets

The ConstraintSets in the MotionScene file allow the full set of ConstraintLayout settings to be applied to a view in terms of positioning, sizing and relation to the parent and other views. In addition, the following attributes may also be included within the ConstraintSet declarations:

  • alpha
  • visibility
  • elevation
  • rotation
  • rotationX
  • rotationY
  • translationX
  • translationY
  • translationZ
  • scaleX
  • scaleY

For example, to rotate the view by 180° during the animation the following could be declared within the start and end constraints:

<ConstraintSet android:id="@+id/start">
    <Constraint
.
.
        motion:layout_constraintStart_toStartOf="parent"
        android:rotation="0">
    </Constraint>
</ConstraintSet>

<ConstraintSet android:id="@+id/end">
    <Constraint
.
.
        motion:layout_constraintBottom_toBottomOf="parent"
        android:rotation="180">
    </Constraint>
</ConstraintSet>

The above changes tell MotionLayout that the view is to start at 0° and then, during the animation, rotate a full 180° before coming to rest upside-down.

Custom Attributes

In addition to the standard attributes listed above, it is also possible to specify a range of custom attributes (declared using CustomAttribute). In fact, just about any property available on the view type can be specified as a custom attribute for inclusion in an animation. To identify the name of the attribute, find the getter/setter name from the documentation for the target view class, remove the get/set prefix and lower the case of the first remaining character. For example, to change the background color of a Button view in code, we might call the setBackgroundColor() setter method as follows:

myButton.setBackgroundColor(Color.RED)

When setting this attribute in a constraint set or key frame, the attribute name will be backgroundColor. In addition to the attribute name, the value must also be declared using the appropriate type from the following list of options:

  • motion:customBoolean – Boolean attribute values.
  • motion:customColorValue – Color attribute values.
  • motion:customDimension –  Dimension attribute values.
  • motion:customFloatValue –  Floating point attribute values.
  • motion:customIntegerValue – Integer attribute values.
  • motion:customStringValue – String attribute values

For example, a color setting will need to be assigned using the customColorValue type:

<CustomAttribute
    motion:attributeName="backgroundColor"
    motion:customColorValue="#43CC76" />

The following excerpt from a MotionScene file, for example, declares start and end constraints for a view in addition to changing the background color from green to red:

.
.
   <ConstraintSet android:id="@+id/start">
        <Constraint
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            motion:layout_editor_absoluteX="21dp"
            android:id="@+id/button"
            motion:layout_constraintTop_toTopOf="parent"
            motion:layout_constraintStart_toStartOf="parent" >
            <CustomAttribute
                motion:attributeName="backgroundColor"
                motion:customColorValue="#33CC33" />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            motion:layout_editor_absoluteY="21dp"
            android:id="@+id/button"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintBottom_toBottomOf="parent" >
            <CustomAttribute
                motion:attributeName="backgroundColor"
                motion:customColorValue="#F80A1F" />
        </Constraint>
    </ConstraintSet>
.
.

Triggering an Animation

Without some form of event to tell MotionLayout to start the animation, none of the settings in the MotionScene file will have any effect on the layout (with the exception that the view will be positioned based on the setting in the start ConstraintSet).

The animation can be configured to start in response to either screen tap (OnClick) or  swipe motion (OnSwipe) gesture. The OnClick handler causes the animation to start and run until completion while OnSwipe will synchronize the animation to move back and forth along the timeline to match the touch motion. The OnSwipe handler will also respond to “flinging” motions on the screen. The OnSwipe handler also provides options to configure how the animation reacts to dragging in different directions and the side of the target view to which the swipe is to be anchored. This allows, for example, left-ward dragging motions to move a view in the corresponding direction while preventing an upward motion from causing a view to move sideways (unless, of course, that is the required behavior).

The OnSwipe and OnClick declarations are contained within the Transition element of a MotionScene file. In both cases the view id must be specified. For example, to implement an OnSwipe handler responding to downward drag motions anchored to the bottom edge of a view named button, the following XML would be placed in the Transition element:

.
.
<Transition
    motion:constraintSetEnd="@+id/end"
    motion:constraintSetStart="@id/start"
    motion:duration="1000">
   <KeyFrameSet>
   </KeyFrameSet>
    <OnSwipe
        motion:touchAnchorId="@+id/button"
        motion:dragDirection="dragDown"
        motion:touchAnchorSide="bottom" />
</Transition>
.
.

Alternatively, to add an OnClick handler to the same button:

<OnClick motion:targetId="@id/button"
    motion:clickAction="toggle" />

In the above example the action has been set to toggle mode. This mode, and the other available options can be summarized as follows:

  • toggle – Animates to the opposite state. For example, if the view is currently at the transition start point it will transition to the end point, and vice versa.
  • jumpToStart – Changes immediately to the start state without animation.
  • jumpToEnd – Changes immediately to the end state without animation.
  • transitionToStart – Transitions with animation to the start state.
  • transitionToEnd – Transitions with animation to the end state.

Arc Motion

By default, a movement of view position will travel in a straight-line between the start and end points. To change the motion to an arc path, simply use the pathMotionArc attribute as follows within the start constraint, configured with either a startHorizontal or startVertical setting to define whether arc is to be concave or convex:

<ConstraintSet android:id="@+id/start">
    <Constraint
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        motion:layout_editor_absoluteX="21dp"
        android:id="@+id/button"
        motion:layout_constraintTop_toTopOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:pathMotionArc="startVertical" >

Figure 43-1 illustrates startVertical and startHorizontal arcs in comparison to the default straight line motion:

Figure 43-1

Keyframes

All of the ConstraintSet attributes outlined so far only apply to the start and end points of the animation. In other words if the rotation property were set to 180° on the end point, the rotation will begin when animation starts and complete when the end point is reached. It is not, therefore, possible to configure the rotation to reach the full 180° at a point 50% of the way through the animation and then rotate back to the original orientation by the end of the animation. Fortunately, this type of effect is available using Keyframes.

Keyframes are used to define intermediate points during the animation at which state changes are to occur. Keyframes could, for example, be declared such that the background color of a view is to have transitioned to blue at a point 50% of the way through the animation, green at the 75% point and then back to the original color by the end of the animation. Keyframes are implemented within the Transition element of the MotionScene file embedded into the KeyFrameSet element.

MotionLayout supports several types of Keyframe which can be summarized as follows:

Attribute Keyframes

Attribute Keyframes (declared using KeyAttribute) allow view attributes to be changed at intermediate points in the animation timeline. KeyAttribute supports the same set of attributes listed above for ConstraintSets combined with the ability to specify where in the animation timeline the change is to take effect. For example, the following Keyframe declaration will cause the button view to double in size gradually both horizontally (scaleX) and vertically (scaleY), reaching full size at a point 50% through the timeline. For the remainder of the timeline, the view will decrease in size to its original dimensions:

<Transition
    motion:constraintSetEnd="@+id/end"
    motion:constraintSetStart="@id/start"
    motion:duration="1000">
   <KeyFrameSet>
       <KeyAttribute
           motion:motionTarget="@+id/button"
           motion:framePosition="50"
           android:scaleX="2.0" />
       <KeyAttribute
           motion:motionTarget="@+id/button"
           motion:framePosition="50"
           android:scaleY="2.0" />
   </KeyFrameSet>

Position Keyframes

Position  keyframes (KeyPosition) are used to modify the path followed by a view as it moves between the start and end locations.  By placing key positions at different points on the timeline, a path of just about any level of complexity can be applied to an animation. Positions are declared using x and y coordinates combined with the corresponding points in the transition timeline. These coordinates must be declared in relation to one of the following coordinate systems:

  • parentRelative – The x and y coordinates are relative to the parent container where the coordinates are specified as a percentage (represented as a value between 0.0 and 1.0):

Figure 43-2

  • deltaRelative – Instead of being relative to the parent, the x and y coordinates are relative to the start and end positions. For example, the start point is (0, 0) the end point (1, 1). Keep in mind that the x and y coordinates can be negative values):

Figure 43-3

  • pathRelative – The x and y coordinates are relative to the path, where the straight line between start and end points serves as the X-axis of the graph. Once again, coordinates are represented as a percentage (0.0 to 1.0). This is similar to the deltaRelative coordinate space but takes into consideration the angle of the path. Once again coordinates may be negative:

Figure 43-4

As an example, the following ConstraintSets declare start and end points on either side of a device screen. By default, a view transition using these points would move in a straight line across the screen as illustrated in Figure 43-5:

Figure 43-5

Suppose, however, that the view is required to follow a path similar to that shown in Figure 43-6 below:

Figure 43-6

To achieve this, key frame position points could be declared within the transition as follows:

<KeyPosition
   motion:motionTarget="@+id/button"
   motion:framePosition="25"
   motion:keyPositionType="pathRelative"
   motion:percentY="0.3"
   motion:percentX="0.25"/>

<KeyPosition
   motion:motionTarget="@+id/button"
   motion:framePosition="75"
   motion:keyPositionType="pathRelative"
   motion:percentY="-0.3"
   motion:percentX="0.75"/>

The above elements create key frame position points 25% and 75% through the path using the pathRelative coordinate system. The first position is placed at coordinates (0.25, 0.3) and the second at (0.75, -0.3). These position key frames can be visualized as illustrated in Figure 43-7 below: 

Figure 43-7

Time Linearity

In the absence of any additional settings, the animations outlined above will be performed at a constant speed. To vary the speed of an animation (for example so that it accelerates and the decelerates) the transition easing attribute (transitionEasing) can be used either within a ConstraintSet or Keyframe.

For complex easing requirements, the linearity can be defined by plotting points on a cubic Bézier curve, for example:

.
.
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:transitionEasing="cubic(0.2, 0.7, 0.3, 1)"
            android:rotation="360">
.
.

If you are unfamiliar with Bézier curves, consider using the curve generator online at the following URL:

https://cubic-bezier.com/

For most requirements, however, easing can be specified using the built-in standard, accelerate and decelerate values:

.
.
    <KeyFrameSet>
           <KeyTrigger
               motion:framePosition="20"
               motion:onPositiveCross="show"
               motion:motionTarget="@id/button"/>
.
.

KeyTrigger

The trigger keyframe (KeyTrigger) allows a method on a view to be called when the animation reaches a specified frame position within the animation timeline. This also takes into consideration the direction of the animations. For example, different methods can be called depending on whether the animation is running forward or backward. Consider a button that is to be made visible when the animation moves beyond 20% of the timeline. The KeyTrigger would be implemented within the KeyFrameSet of the Transition element as follows using the onPositiveCross property:

.
.
    <KeyFrameSet>
           <KeyTrigger
               motion:framePosition="20"
               motion:onPositiveCross="show"
               motion:motionTarget="@id/button"/>
.
.

Similarly, if the same button is to be hidden when the animation is reversed and drops below the 10%, a second key trigger could be added using the onNegativeCross property:

<KeyTrigger
     motion:framePosition="20"
     motion:onNegativeCross="show"
     motion:motionTarget="@id/button2"/>

If the animation is using toggle action, simply use the onCross property:

<KeyTrigger
     motion:framePosition="20"
     motion:onCross="show"
     motion:motionTarget="@id/button2"/>

While position keyframes can be used to add intermediate state changes into the animation this would quickly become cumbersome if large numbers of repetitive positions and changes needed to be implemented.  For situations where state changes need to be performed repetitively with predictable changes, MotionLayout includes the Cycle and Time Cycle keyframes, a topic which will be covered in detail in the chapter entitled “A MotionLayout KeyCycle Tutorial”.

Starting an Animation from Code

So far in this chapter we have only looked at controlling an animation using the OnSwipe and OnClick  handlers. It is also possible to start an animation from within code by calling methods on the MotionLayout instance. The following code, for example, runs the transition from start to end with a duration of 2000ms for a layout named motionLayout:

motionLayout.setTransitionDuration(2000)
motionLayout.transitionToEnd()

In the absence of addition settings, the start and end states used for the animation will be those declared in the Transition declaration of the MotionScene file. To use specific start and end constraint sets, simply reference them by id in a call to the setTransition() method of the MotionLayout instance:

motionLayout.setTransition(R.id.myStart, R.id.myEnd)
motionLayout.transitionToEnd()

To monitor the state of an animation while it is running, add a transition listener to the MotionLayout instance as follows:

motionLayout.setTransitionListener(
    object: MotionLayout.TransitionListener {

        override fun onTransitionTrigger(motionLayout: MotionLayout?, 
                    triggerId: Int, positive: Boolean, progress: Float) {
		// Called when a trigger keyframe threshold is crossed
        }

        override fun onTransitionStarted(motionLayout: MotionLayout?, 
                    startId: Int, endId: Int) {
		// Called when the transition starts
        }

        override fun onTransitionChange(motionLayout: MotionLayout?, 
                    startId: Int, endId: Int, progress: Float) {
		// Called each time a property changes. Track progress value to find 
		// current position
        }

        override fun onTransitionCompleted(motionLayout: MotionLayout?,
                                           currentId: Int) {
		// Called when the transition is complete
        }
    })

Summary

MotionLayout is a subclass of ConstraintLayout designed specifically to add animation effects to the views in user interface layouts. MotionLayout works by animating the transition of a view between two states defined by start and end constraint sets. Additional animation effects may be added between these start and end points by making use of keyframes.

Animations may be triggered either via OnClick or OnSwipe handlers or programmatically via method calls on the MotionLayout instance.