A Basic Overview of Java Threads, Handlers and Executors

The next chapter will be the first in a series of chapters intended to introduce the use of Android Services to perform application tasks in the background. It is impossible, however, to understand the steps involved in implementing services without first gaining a basic understanding of the concept of threading in Android applications. Threads, thread handlers and execution services are, therefore, the topic of this chapter.

An Overview of Threads

Threads are the cornerstone of any multitasking operating system and can be thought of as mini-processes running within a main process, the purpose of which is to enable at least the appearance of parallel execution paths within applications.

The Application Main Thread

When an Android application is first started, the runtime system creates a single thread in which all application components will run by default. This thread is generally referred to as the main thread. The primary role of the main thread is to handle the user interface in terms of event handling and interaction with views in the user interface. Any additional components that are started within the application will, by default, also run on the main thread.

Any component within an application that performs a time consuming task using the main thread will cause the entire application to appear to lock up until the task is completed. This will typically result in the operating system displaying an “Application is not responding” warning to the user. Clearly, this is far from the desired behavior for any application. This can be avoided simply by launching the task to be performed in a separate thread, allowing the main thread to continue unhindered with other tasks.

Thread Handlers

Clearly, one of the key rules of Android development is to never perform time-consuming operations on the main thread of an application. The second, equally important, rule is that the code within a separate thread must never, under any circumstances, directly update any aspect of the user interface.

Any changes to the user interface must always be performed from within the main thread. The reason for this is that the Android UI toolkit is not thread-safe. Attempts to work with non-thread-safe code from within multiple threads will typically result in intermittent problems and unpredictable application behavior.

In the event that the code executing in a thread needs to interact with the user interface, it must do so by synchronizing with the main UI thread. This is achieved by creating a handler within the main thread, which, in turn, receives messages from another thread and updates the user interface accordingly.

A Threading Example

The remainder of this chapter will work through some simple examples intended to provide a basic introduction to threads. The first step will be to highlight the importance of performing time-consuming tasks in a separate thread from the main thread.

Launch Android Studio, select the Create New Project quick start option from the Android Studio welcome screen and, within the resulting new project dialog, choose the Empty Activity template before clicking on the Next button.

Enter ThreadExample into the Name field and specify com.ebookfrenzy.threadexample 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.

Building the App

Load the activity_main.xml file for the project into the Layout Editor tool. Select the default TextView component and change the ID for the view to myTextView in the Properties tool window.

Add a Button view to the user interface positioned directly beneath the existing TextView object as illustrated in Figure 63-1. Once the button has been added, click on the Infer Constraints button in the toolbar to add the missing constraints.

Change the text to “Press Me” and extract the string to a resource named press_me. With the button view still selected in the layout, locate the onClick property and enter buttonClick as the method name.

Figure 63-1

Next, load the MainActivity.java file into an editing panel and add code for the buttonClick() method which will be called when the Button view is tapped by the user. Since the goal here is to demonstrate the problem of performing lengthy tasks on the main thread, the code will simply pause for 20 seconds before displaying different text on the TextView object:

package com.ebookfrenzy.threadexample;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    TextView myTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        myTextView = findViewById(R.id.myTextView);
    }

    public void buttonClick(View view) {

        long endTime = System.currentTimeMillis() + 20 * 1000;
        while (System.currentTimeMillis() < endTime) {
            synchronized (this) {
                try {
                    wait(endTime - System.currentTimeMillis());
                } catch (Exception e) {
                }
            }
        }
        myTextView.setText("Button Pressed");
    }
}

With the code changes complete, run the application on either a physical device or an emulator. Once the application is running, tap the button, at which point the application will appear to freeze. It will, for example, not be possible to touch the button a second time and in some situations the operating system will report the application as being unresponsive as shown in Figure 63-2.

Figure 63-2

Clearly, anything that is going to take time to complete within the buttonClick() method needs to be performed within a separate thread.

Creating a New Thread

In order to create a new thread, the code to be executed in that thread needs to be placed within the Run() method of a Runnable instance. A new Thread object then needs to be created, passing through a reference to the Runnable instance to the constructor. Finally, the start() method of the thread object needs to be called to start the thread running. To perform the task within the buttonClick() method, therefore, the following changes need to be made:

public void buttonClick(View view) {

    Runnable runnable = new Runnable() {
        public void run() {
            long endTime = System.currentTimeMillis() + 20 * 1000;
            while (System.currentTimeMillis() < endTime) {
                synchronized (this) {
                    try {
                        wait(endTime - System.currentTimeMillis());
                    } catch (Exception e) {
                    }
                }
            }
        }
    };
    Thread myThread = new Thread(runnable);
    myThread.start();
}

In fact, the runnable declaration can be simplified if desired by making use of a Java lambda expression. Making this change would result in the following declaration:

.
.
        Runnable runnable = () -> {
            long endTime = System.currentTimeMillis() + 20 * 1000;
                while (System.currentTimeMillis() < endTime) {
                    synchronized (this) {
                        try {
                            wait(endTime - System.currentTimeMillis());
                        } catch (Exception e) {
                        }
                    }
                }
        };
        Thread myThread = new Thread(runnable);
        myThread.start();
.
.

When the application is now run, touching the button causes the delay to be performed in a new thread leaving the main thread to continue handling the user interface, including responding to additional button presses. In fact, each time the button is touched, a new thread will be created, allowing the task to be performed multiple times concurrently.

A close inspection of the updated code for the buttonClick() method will reveal that the code to update the TextView has been removed. As previously stated, updating a user interface element from within a thread other than the main thread violates a key rule of Android development. In order to update the user interface, therefore, it will be necessary to implement a Handler for the thread.

Implementing a Thread Handler

Thread handlers are implemented in the main thread of an application and are primarily used to make updates to the user interface in response to messages sent by other threads running within the application’s process.

Handlers are subclassed from the Android Handler class and can be used either by specifying a Runnable to be executed when required by the thread, or by overriding the handleMessage() callback method within the Handler subclass which will be called when messages are sent to the handler by a thread.

For the purposes of this example, a handler will be implemented to update the user interface from within the previously created thread. Load the MainActivity.java file into the Android Studio editor and modify the code to add a Handler instance to the activity:

.
.
import android.os.Handler;
import android.os.Message;
import android.os.Looper;

public class MainActivity extends AppCompatActivity {
.
.
    Handler handler = new Handler(Looper.getMainLooper()) {
        @Override public void handleMessage(Message msg) {
            myTextView.setText("Message Received");
        }
    };
.
.

The above code changes have declared a handler and implemented within that handler the handleMessage() callback which will be called when the thread sends the handler a message. In this instance, the code simply displays a string on the TextView object in the user interface.

All that now remains is to modify the thread created in the buttonClick() method to send a message to the handler when the delay has completed:

public void buttonClick(View view) {

    Runnable runnable = () -> {
            long endTime = System.currentTimeMillis() + 20 * 1000;
            while (System.currentTimeMillis() < endTime) {
                synchronized (this) {
                    try {
                        wait(endTime - System.currentTimeMillis());
                    } catch (Exception e) {
                    }
                }
            }
        }
        handler.sendEmptyMessage(0);
    };
    Thread myThread = new Thread(runnable);
    myThread.start();
}

Note that the only change that has been made is to make a call to the sendEmptyMessage() method of the handler. Since the handler does not currently do anything with the content of any messages it receives it is sent an empty message object.

Compile and run the application and, once executing, touch the button. After a 20 second delay, the new text will appear in the TextView object in the user interface.

Passing a Message to the Handler

While the previous example triggered a call to the handleMessage() handler callback, it did not take advantage of the message object to send data to the handler. In this phase of the tutorial, the example will be further modified to pass data between the thread and the handler. First, the updated thread in the buttonClick() method will obtain the date and time from the system in string format and store that information in a Bundle object. A call will then be made to the obtainMessage() method of the handler object to get a message object from the message pool. Finally, the bundle will be added to the message object before being sent via a call to the sendMessage() method of the handler object:

public void buttonClick(View view) {

    Runnable runnable = () -> {
       long endTime = System.currentTimeMillis() + 20 * 1000;
       while (System.currentTimeMillis() < endTime) {
            synchronized (this) {
                try {
                    wait(endTime - System.currentTimeMillis());
                } catch (Exception e) {
                }
            }
        }
        Message msg = handler.obtainMessage();
        Bundle bundle = new Bundle();
        bundle.putString("myKey", "Thread Completed");
        msg.setData(bundle);
        handler.sendMessage(msg);
    };
    Thread myThread = new Thread(runnable);
    myThread.start();
}

Next, update the handleMessage() method of the handler to extract the date and time string from the bundle object in the message and display it on the TextView object:

Handler handler = new Handler(Looper.getMainLooper()) {
    @Override
    public void handleMessage(Message msg) {
        Bundle bundle = msg.getData(); 
        String string = bundle.getString("myKey");
        myTextView.setText(string);
    }
};

Finally, compile and run the application and test that touching the button now causes the “Thread Complete” message to appear on the TextView object after the thread finishes.

Java Executor Concurrency

So far in this chapter we have looked exclusively at directly creating and managing Java threads. While acceptable for simple multi-threading tasks, this can prove to be inadequate when working with complex situations involving large number of threads. There is, for example, a system overhead involved in starting and stopping threads. An app that creates and destroys large number of threads is, therefore, at risk of exhibiting degraded performance. The basic threading API also does not provide pre-built options for scheduling or repeating task execution, or for returning results from a task.

The shortcomings of working directly with threads can be overcome by making use of the Executor classes of the Java Concurrent framework (part of the java.util.concurrent package). This framework allows for a pool of active threads to be created and manages how tasks are assigned to those threads. This allows existing threads to be reused for other tasks without the need to constantly create new threads.

This framework also provides additional functionality including the ability to return a result on completion of a task (referred to as a Callable task), check the status of a thread and to schedule tasks to run either after a timeout or at repeated time intervals.

Working with Runnable Tasks

The first step in exploring this framework is to modify the buttonClicked() method to use a concurrency framework Executor to run the task in a separate thread:

.
.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
.
.
    public void buttonClick(View view) {

        ExecutorService executor = Executors.newSingleThreadExecutor();

        executor.submit(new Runnable(){
            public void run(){
                long endTime = System.currentTimeMillis() + 10 * 1000;
                while (System.currentTimeMillis() < endTime) {
                    synchronized (this) {
                        try {
                            wait(endTime - System.currentTimeMillis());
                        } catch (Exception e) {
                        }
                    }
                }
                Message msg = handler.obtainMessage();
                Bundle bundle = new Bundle();
                bundle.putString("myKey", "Button Pressed");
                msg.setData(bundle);
                handler.sendMessage(msg);
            }
        });
        executor.shutdown();
    }
.
.

When the above code is executed, the timeout will be performed on a separate thread as before. The changes made to the method, however, require some explanation. First, a reference to an ExecutorService instance is obtained from the system Executors instance:

ExecutorService executor = Executors.newSingleThreadExecutor();

In this case, a pool containing only one thread is requested. A pool with a specified number of threads could have been requested as follows:

ExecutorService executor = Executors.newFixedThreadPool(10);

Next, a Runnable task is started on the thread via a call to the submit() method of the executor service instance:

executor.submit(new Runnable(){ public void run()
{ 
    long endTime = System.currentTimeMillis() + 20 * 1000; 
.
.

Note that the above declaration can be simplified by converting it to a lambda as follows:

executor.submit(() -> {
    long endTime = System.currentTimeMillis() + 20 * 1000;
.
.

From this point on, the task will run until completion. Once completed however, the executor service will continue to run. If you have no further use for the service, it should be shutdown.

Shutting down an Executor Service

ExecutorService provides a few techniques for initiating a shutdown. To notify the service that it should shutdown automatically after the currently running tasks have reached completion, a call to the shutdown() method should be made as follows:

executor.shutdown();

A call to the shutdownNow() method, on the other hand, stops all tasks running on the service and, cancels the processing of pending tasks:

executor.shutdownNow();

Working with Callable Tasks and Futures

As previously mentioned, the ExecutorService supports so called “Callable” tasks which are able to return a result after the task is completed. Tasks running on separate thread are typically expected to take some time to complete (otherwise they probably would not need to run on a separate thread in the first place). This raises the question of how the result is returned to the code in the thread from which the task was launched. This is achieved using the Future value type which represents a value which will be provided at some point in the future.

When a callable task is executed it returns a Future instance which may then be used by the app to obtain the result when the task completes. To see this in action, begin by editing the activity_main.xml file to add an additional button labeled “Status” with the onClick property configured to call method named statusClick:

Figure 63-3

Next, modify the buttonClick() method to execute a Callable task configured to return a String value via a Future variable:

.
.
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
.
.
public class MainActivity extends AppCompatActivity {

    TextView myTextView;
    Future<String> future;
.
.
   public void buttonClick(View view) {

        ExecutorService executor = Executors.newSingleThreadExecutor();

        future = executor.submit(new Callable<String>() {
            public String call() {
                long endTime = System.currentTimeMillis() + 10 * 1000;
                while (System.currentTimeMillis() < endTime) {
                    synchronized (this) {
                        try {
                            wait(endTime - System.currentTimeMillis());
                        } catch (Exception e) {
                        }
                    }
                }
                return("Task Completed");
            }
        });
        executor.shutdown();
    }
.
.

Note that in addition to importing the java.util.concurrent.Future package and declaring a Future variable for storing a string value, some changes have also been made to the way in which the task is launched:

future = executor.submit(new Callable<String>() {
            public String call() {

The key points here are that instead of submitting a Runnable task to the executor service, we are now passing through a Callable task (declared to return a String value). Note also that the result of the task is assigned to the Future variable. In addition, call() is used instead of the run() method used previously when submitting a Runnable task. Finally, a return statement has been added to return a string value:

return("Task Completed") 

Handling a Future Result

The buttonClick() method is now configured to launch a Callable task with the return value assigned to the Future variable. The app now needs to know when the task is complete and the result available. One option is to call the get() method of the Future variable. Since this method is able to throw exceptions in the event that the execution fails or is interrupted, this must be performed in a try/catch statement as follows:

String result = null;

try {
    result = future.get();
} catch (ExecutionException | InterruptedException e) {
    e.printStackTrace();
}

Unfortunately, the get() method will block the current thread until the task running in the thread completes, thereby defeating the purpose of running the task in a separate thread in the first place. Another option is to provide the get() method call with a timeout after which it will return control to the current thread. The following code, for example, will cause the get() call to timeout after 3 seconds:

result = future.get(3, TimeUnit.SECONDS);

A better alternative, however, is to call the isDone() method of the Future instance to check the status of the thread and only call the get() method once the task is complete. To implement this behavior, add the statusClick() method to the MainActivity.java file as follows:

.
.
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
.
.
public void statusClick(View view) {

    if (future.isDone()) {
        String result = null;
        try {
            result = future.get(3, TimeUnit.SECONDS);
        } catch (ExecutionException | InterruptedException 
                     | TimeoutException e) {
            e.printStackTrace();
        }
        myTextView.setText(result);
   } else {
        myTextView.setText("Waiting");
   }
}

With the changes made, run the app and click on the “Press Me” button. While the task is running, click on the Status button. As long as the task is still running, the “Waiting” message will be displayed in the TextView. Once the task completes, however, the isDone() method will return a true value, the get() method will be called and the string returned by the task (“Task Complete”) displayed on the TextView.

Scheduling Tasks

The final area to be covered involves the use of ExecutorService to schedule task execution. This involves use of a ScheduledExecutorService instance on which the schedule() method needs to be called passing through the Runnable task to be executed together with a time delay. The schedule() call will return a ScheduledFuture instance which may be used to identify the remaining time before the task is due to start.

The following code, for example, schedules a task to run after a 30 second delay and accesses the remaining delay time:

ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

Runnable task = () -> {
	// Code to perform task here
};

ScheduledFuture<?> future = executor.schedule(task, 30, TimeUnit.SECONDS);
long delayRemaining = future.getDelay(TimeUnit.SECONDS);

Similarly, the ScheduledExecutorService may be used to execute a task repeatedly at regular intervals starting after an optional initial delay. The following code, for example, causes a task to be performed every 10 seconds after an initial 30 second delay:

executor.scheduleAtFixedRate(task, 30, 10, TimeUnit.SECONDS);

The scheduleAtFixedRate() method will launch the next instance of the task regardless of whether or not the previously scheduled task has completed. To specify a fixed period of time between the end of a task execution and the start of the next execution, use the scheduleWithFixedDelay() method. In the following example, the first task is scheduled after a 0 second delay, with each subsequent execution taking place 10 seconds after completion of the proceeding task:

executor.scheduleWithFixedDelay(task, 0, 10, TimeUnit.SECONDS);

Summary

The goal of this chapter was to provide an overview of threading within Android applications. When an application is first launched in a process, the runtime system creates a main thread in which all subsequently launched application components run by default. The primary role of the main thread is to handle the user interface, so any time consuming tasks performed in that thread will give the appearance that the application has locked up. It is essential, therefore, that tasks likely to take time to complete be started in a separate thread.

Because the Android user interface toolkit is not thread-safe, changes to the user interface should not be made in any thread other than the main thread. User interface changes can be implemented by creating a handler in the main thread to which messages may be sent from within other, non-main threads.

Threads can be created either directly, or using the executor services of the Java Concurrent framework. For more complex threading requirements, this framework provides automatic management of thread pools, returning of results from tasks and execution scheduling.

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.