As your app is used, new information will need to be shown on the screen while old information is removed. Immediately switching what's shown can look jarring or users can easily miss the new content on the screen. Utilizing animations can slow down the changes and draw the user's eye with notion so the updates are more apparent.
There are three common animations to use when showing or hiding a view. You can use the circular reveal animation, a crossfade animation, or a cardflip animation.
Create a crossfade animation
Crossfade animations (also known as dissolve) gradually fade out one View
or ViewGroup
while simultaneously fading
in another. This animation is useful for situations where you want to switch
content or views in your app. The crossfade animation shown here uses ViewPropertyAnimator
, which is available for Android 3.1 (API
level 12) and higher.
Here's an example of a crossfade from a progress indicator to some text content.
Create the views
First, you need to create the two views that you want to crossfade. The following example creates a progress indicator and a scrollable text view:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView style="?android:textAppearanceMedium"
android:lineSpacingMultiplier="1.2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/lorem_ipsum"
android:padding="16dp" />
</ScrollView>
<ProgressBar android:id="@+id/loading_spinner"
style="?android:progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
Set up the crossfade animation
To set up the crossfade animation:
- Create member variables for the views that you want to crossfade. You need these references later when modifying the views during the animation.
- For the view that is being faded in, set its visibility to
GONE
. This prevents the view from taking up layout space and omits it from layout calculations, speeding up processing. - Cache the
system property in a member variable. This property defines a standard "short" duration for the animation. This duration is ideal for subtle animations or animations that occur very frequently.config_shortAnimTime
config_longAnimTime
andconfig_mediumAnimTime
are also available if you wish to use them.
Here's an example using the layout from the previous code snippet as the activity content view:
Kotlin
class CrossfadeActivity : Activity() { private lateinit var contentView: View private lateinit var loadingView: View private var shortAnimationDuration: Int = 0 ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_crossfade) contentView = findViewById(R.id.content) loadingView = findViewById(R.id.loading_spinner) // Initially hide the content view. contentView.visibility = View.GONE // Retrieve and cache the system's default "short" animation time. shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime) } ... }
Java
public class CrossfadeActivity extends Activity { private View contentView; private View loadingView; private int shortAnimationDuration; ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_crossfade); contentView = findViewById(R.id.content); loadingView = findViewById(R.id.loading_spinner); // Initially hide the content view. contentView.setVisibility(View.GONE); // Retrieve and cache the system's default "short" animation time. shortAnimationDuration = getResources().getInteger( android.R.integer.config_shortAnimTime); } ... }
Crossfade the views
Now that the views are properly set up, crossfade them by doing the following:
- For the view that is fading in, set the alpha value to
0
and the visibility toVISIBLE
. (Remember that it was initially set toGONE
.) This makes the view visible but completely transparent. - For the view that is fading in, animate its alpha value from
0
to1
. For the view that is fading out, animate the alpha value from1
to0
. - Using
onAnimationEnd()
in anAnimator.AnimatorListener
, set the visibility of the view that was fading out toGONE
. Even though the alpha value is0
, setting the view's visibility toGONE
prevents the view from taking up layout space and omits it from layout calculations, speeding up processing.
The following method shows an example of how to do this:
Kotlin
class CrossfadeActivity : Activity() { private lateinit var contentView: View private lateinit var loadingView: View private var shortAnimationDuration: Int = 0 ... private fun crossfade() { contentView.apply { // Set the content view to 0% opacity but visible, so that it is visible // (but fully transparent) during the animation. alpha = 0f visibility = View.VISIBLE // Animate the content view to 100% opacity, and clear any animation // listener set on the view. animate() .alpha(1f) .setDuration(shortAnimationDuration.toLong()) .setListener(null) } // Animate the loading view to 0% opacity. After the animation ends, // set its visibility to GONE as an optimization step (it won't // participate in layout passes, etc.) loadingView.animate() .alpha(0f) .setDuration(shortAnimationDuration.toLong()) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { loadingView.visibility = View.GONE } }) } }
Java
public class CrossfadeActivity extends Activity { private View contentView; private View loadingView; private int shortAnimationDuration; ... private void crossfade() { // Set the content view to 0% opacity but visible, so that it is visible // (but fully transparent) during the animation. contentView.setAlpha(0f); contentView.setVisibility(View.VISIBLE); // Animate the content view to 100% opacity, and clear any animation // listener set on the view. contentView.animate() .alpha(1f) .setDuration(shortAnimationDuration) .setListener(null); // Animate the loading view to 0% opacity. After the animation ends, // set its visibility to GONE as an optimization step (it won't // participate in layout passes, etc.) loadingView.animate() .alpha(0f) .setDuration(shortAnimationDuration) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { loadingView.setVisibility(View.GONE); } }); } }
Create a card flip animation
Card flips animate between views of content by showing an animation that emulates
a card flipping over. The card flip animation shown here uses FragmentTransaction
, which is available for Android 3.0 (API
level 11) and higher.
Here's what a card flip looks like:
Create the Animator objects
In order to create the card flip animation, you need a total of four animators. Two animators for when the front of the card animates out and to the left and in and from the left. You also need two animators for when the back of the card animates in and from the right and out and to the right.
card_flip_left_in.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Before rotating, immediately set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:duration="0" />
<!-- Rotate. -->
<objectAnimator
android:valueFrom="-180"
android:valueTo="0"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Half-way through the rotation (see startOffset), set the alpha to 1. -->
<objectAnimator
android:valueFrom="0.0"
android:valueTo="1.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
card_flip_left_out.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Rotate. -->
<objectAnimator
android:valueFrom="0"
android:valueTo="180"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Half-way through the rotation (see startOffset), set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
card_flip_right_in.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Before rotating, immediately set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:duration="0" />
<!-- Rotate. -->
<objectAnimator
android:valueFrom="180"
android:valueTo="0"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Half-way through the rotation (see startOffset), set the alpha to 1. -->
<objectAnimator
android:valueFrom="0.0"
android:valueTo="1.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
card_flip_right_out.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Rotate. -->
<objectAnimator
android:valueFrom="0"
android:valueTo="-180"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Half-way through the rotation (see startOffset), set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
Create the views
Each side of the "card" is a separate layout that can contain any content you want, such as two text views, two images, or any combination of views to flip between. You'll then use the two layouts in the fragments that you'll later animate. The following layouts create one side of a card that shows text:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#a6c"
android:padding="16dp"
android:gravity="bottom">
<TextView android:id="@android:id/text1"
style="?android:textAppearanceLarge"
android:textStyle="bold"
android:textColor="#fff"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/card_back_title" />
<TextView style="?android:textAppearanceSmall"
android:textAllCaps="true"
android:textColor="#80ffffff"
android:textStyle="bold"
android:lineSpacingMultiplier="1.2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/card_back_description" />
</LinearLayout>
And the other side of the card that displays an ImageView
:
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/image1"
android:scaleType="centerCrop"
android:contentDescription="@string/description_image_1" />
Create the fragments
Create fragment classes for the front and back of the card. These classes return the layouts
that you created previously in the onCreateView()
method
of each fragment. You can then create instances of this fragment in the parent activity
where you want to show the card. The following example shows nested fragment classes inside
of the parent activity that uses them:
Kotlin
class CardFlipActivity : FragmentActivity() { ... /** * A fragment representing the front of the card. */ class CardFrontFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View = inflater.inflate(R.layout.fragment_card_front, container, false) } /** * A fragment representing the back of the card. */ class CardBackFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View = inflater.inflate(R.layout.fragment_card_back, container, false) } }
Java
public class CardFlipActivity extends FragmentActivity { ... /** * A fragment representing the front of the card. */ public class CardFrontFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_card_front, container, false); } } /** * A fragment representing the back of the card. */ public class CardBackFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_card_back, container, false); } } }
Animate the card flip
Now, you'll need to display the fragments inside of a parent activity.
To do this, first create the layout for your activity. The following example creates a
FrameLayout
that you
can add fragments to at runtime:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
In the activity code, set the content view to be the layout that you just created. It's also a good idea to show a default fragment when the activity is created, so the following example activity shows you how to display the front of the card by default:
Kotlin
class CardFlipActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_activity_card_flip) if (savedInstanceState == null) { supportFragmentManager.beginTransaction() .add(R.id.container, CardFrontFragment()) .commit() } } ... }
Java
public class CardFlipActivity extends FragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_activity_card_flip); if (savedInstanceState == null) { getSupportFragmentManager() .beginTransaction() .add(R.id.container, new CardFrontFragment()) .commit(); } } ... }
Now that you have the front of the card showing, you can show the back of the card with the flip animation at an appropriate time. Create a method to show the other side of the card that does the following things:
- Sets the custom animations that you created earlier for the fragment transitions.
- Replaces the currently displayed fragment with a new fragment and animates this event with the custom animations that you created.
- Adds the previously displayed fragment to the fragment back stack so when the user presses the Back button, the card flips back over.
Kotlin
class CardFlipActivity : FragmentActivity() { ... private fun flipCard() { if (showingBack) { supportFragmentManager.popBackStack() return } // Flip to the back. showingBack = true // Create and commit a new fragment transaction that adds the fragment for // the back of the card, uses custom animations, and is part of the fragment // manager's back stack. supportFragmentManager.beginTransaction() // Replace the default fragment animations with animator resources // representing rotations when switching to the back of the card, as // well as animator resources representing rotations when flipping // back to the front (e.g. when the system Back button is pressed). .setCustomAnimations( R.animator.card_flip_right_in, R.animator.card_flip_right_out, R.animator.card_flip_left_in, R.animator.card_flip_left_out ) // Replace any fragments currently in the container view with a // fragment representing the next page (indicated by the // just-incremented currentPage variable). .replace(R.id.container, CardBackFragment()) // Add this transaction to the back stack, allowing users to press // Back to get to the front of the card. .addToBackStack(null) // Commit the transaction. .commit() } }
Java
public class CardFlipActivity extends FragmentActivity { ... private void flipCard() { if (showingBack) { getSupportFragmentManager().popBackStack(); return; } // Flip to the back. showingBack = true; // Create and commit a new fragment transaction that adds the fragment for // the back of the card, uses custom animations, and is part of the fragment // manager's back stack. getSupportFragmentManager() .beginTransaction() // Replace the default fragment animations with animator resources // representing rotations when switching to the back of the card, as // well as animator resources representing rotations when flipping // back to the front (e.g. when the system Back button is pressed). .setCustomAnimations( R.animator.card_flip_right_in, R.animator.card_flip_right_out, R.animator.card_flip_left_in, R.animator.card_flip_left_out) // Replace any fragments currently in the container view with a // fragment representing the next page (indicated by the // just-incremented currentPage variable). .replace(R.id.container, new CardBackFragment()) // Add this transaction to the back stack, allowing users to press // Back to get to the front of the card. .addToBackStack(null) // Commit the transaction. .commit(); } }
Create a circular reveal animation
Reveal animations provide users visual continuity when you show or hide a group of UI
elements. The ViewAnimationUtils.createCircularReveal()
method enables you to animate a clipping circle to
reveal or hide a view. This animation is provided in the
ViewAnimationUtils
class, which is available for
Android 5.0 (API level 21) and higher.
Here is an example showing how to reveal a previously invisible view:
Kotlin
// previously invisible view val myView: View = findViewById(R.id.my_view) // Check if the runtime version is at least Lollipop if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // get the center for the clipping circle val cx = myView.width / 2 val cy = myView.height / 2 // get the final radius for the clipping circle val finalRadius = Math.hypot(cx.toDouble(), cy.toDouble()).toFloat() // create the animator for this view (the start radius is zero) val anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0f, finalRadius) // make the view visible and start the animation myView.visibility = View.VISIBLE anim.start() } else { // set the view to invisible without a circular reveal animation below Lollipop myView.visibility = View.INVISIBLE }
Java
// previously invisible view View myView = findViewById(R.id.my_view); // Check if the runtime version is at least Lollipop if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // get the center for the clipping circle int cx = myView.getWidth() / 2; int cy = myView.getHeight() / 2; // get the final radius for the clipping circle float finalRadius = (float) Math.hypot(cx, cy); // create the animator for this view (the start radius is zero) Animator anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0f, finalRadius); // make the view visible and start the animation myView.setVisibility(View.VISIBLE); anim.start(); } else { // set the view to invisible without a circular reveal animation below Lollipop myView.setVisibility(View.INVISIBLE); }
The ViewAnimationUtils.createCircularReveal()
animation takes five parameters. The first parameter is
the view that you want to either hide or show on screen. The next two parameters are the x and y
coordinates for the center of the clipping circle. Typically this will be the center of the view,
but you can also use the point the user touched so the animation starts where they selected. The
fourth parameter is the starting radius of the clipping circle.
In the above example, the initial radius is set to 0 so the view to be displayed will be hidden by the circle. The last parameter is the final radius of the circle. When displaying a view make sure the final radius is larger than the view itself so the view can be fully revealed before the animation finishes.
To hide a previously visible view:
Kotlin
// previously visible view val myView: View = findViewById(R.id.my_view) // Check if the runtime version is at least Lollipop if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // get the center for the clipping circle val cx = myView.width / 2 val cy = myView.height / 2 // get the initial radius for the clipping circle val initialRadius = Math.hypot(cx.toDouble(), cy.toDouble()).toFloat() // create the animation (the final radius is zero) val anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, initialRadius, 0f) // make the view invisible when the animation is done anim.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) myView.visibility = View.INVISIBLE } }) // start the animation anim.start() } else { // set the view to visible without a circular reveal animation below Lollipop myView.visibility = View.VISIBLE }
Java
// previously visible view final View myView = findViewById(R.id.my_view); // Check if the runtime version is at least Lollipop if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // get the center for the clipping circle int cx = myView.getWidth() / 2; int cy = myView.getHeight() / 2; // get the initial radius for the clipping circle float initialRadius = (float) Math.hypot(cx, cy); // create the animation (the final radius is zero) Animator anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, initialRadius, 0f); // make the view invisible when the animation is done anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); myView.setVisibility(View.INVISIBLE); } }); // start the animation anim.start(); } else { // set the view to visible without a circular reveal animation below Lollipop myView.setVisibility(View.VISIBLE); }
In this case the initial radius of the clipping circle is set to be as large as the view so the
view will be visible before the animation starts. The final radius is set to 0 so the view will
be hidden when the animation finishes. It's important to add a listener to the animation so the
view's visibility can be set to INVISIBLE
when the animation
completes.