Skip to content

Animation

Malachi de AElfweald edited this page Dec 10, 2018 · 43 revisions

Goal

Explore some basic animations.

Table of Contents

Let's explore some animations.

Card Flips

With a card flip animation, we can make it look like your reading list and edit book form are two sides of a card (like from a deck of cards).

For this, we will use the XML resources in the Android Developer site tutorial. Unfortunately, that tutorial is missing a few pieces that we will need to add.

Right-click on your res folder and choose New | Android resource directory. On the Resource type field, choose animator then click OK.

You may have to scroll up to see animator. They are alphabetical.

card_flip_left_in.xml

Right-click on the new animator directory and choose New | Animator resource file.

New Animation Resource

Give it a name of card_flip_left_in.xml and click OK.

Copy over this snippet from the Android tutorial:

<?xml version="1.0" encoding="utf-8"?>
<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>

There are three property changes that happen as part of this animation resource.

The first one changes the alpha (ie: transparency) from full (1.0) to none (0.0) with no delay (duration of 0).

The second one rotates around the Y axis from -180 degrees to 0 degrees. IE we are going to flip to the back of the card.

The third one sets the alpha back to full, but with a duration, which means we'll see it happen.

The IDE will give you an error because @integer/card_flip_time_full and @integer/card_flip_time_half are not defined.

With a little bit of sleuthing, we can determine what the intention was for these values. The AOSP shows that full should be 300 and half should be 150.

integers.xml

Let's create them.

Click the card_flip_time_full and use the IDE Quick Fix to Create integer value resource.

Remember you can access the IDE quick-fix by clicking on the light bulb or pressing Alt+Enter ( Option+Enter on Mac)

Set value

Set the value to 300. Note that this screen doesn't tell you which value you are editing, so if you are unsure, cancel and redo the process.

Do the same process for the card_flip_time_half, setting it to 150.

To confirm what it created, Ctrl+Click on card_flip_time_half to open the new integers.xml file.

card_flip_left_out.xml

Create another animation resource, card_flip_left_out.xml.

<?xml version="1.0" encoding="utf-8"?>
<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>

This one rotates the Y axes from 0 back to 180, with a delay turning off the transparency.

card_flip_right_in.xml

Next we create card_flip_right_in.xml.

<?xml version="1.0" encoding="utf-8"?>
<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>

This one is similar to the first one but starts at 180 degrees instead of -180 degrees.

card_flip_right_out.xml

Lastly, create the card_flip_right_out.xml.

<?xml version="1.0" encoding="utf-8"?>
<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>

As you might expect, this one is similar to our second one, but uses -180 degrees instead of 180 degrees.

Update BookListFragment

There are two places in BookListFragment where we call beginTransaction. For each of them, we will want to specify our custom animation.

Open BookListFragment.

our viewAdapter

Update your viewAdapter

private val viewAdapter = ReadingListAdapter(
    bookClicked = { book ->
        app?.let { readingListApp ->
            fragManager?.let {
                it.beginTransaction()
                    .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(R.id.container, EditBookFragment.newInstance(
                    app = readingListApp,
                    source= book))
                .addToBackStack(null)
                .commit()
            }
        }
    }
)

our FAB

Update your fab in onCreateView

binding.fab.setOnClickListener {
    app?.let { readingListApp ->
        fragManager?.let {
            it.beginTransaction()
                .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(R.id.container, EditBookFragment.newInstance(app = readingListApp))
            .addToBackStack(null)
            .commit()
        }
    }
}

Deploy your application and edit a book.

You'll notice that only the reading list portion of the page swaps. The title bar remains in place. That's because the title bar is defined at the activity-level and the reading list is at the fragment level.

Cross Fades

What if you would like to cross fade between two different views?

For our application, let's look at the idea of providing some feedback while we are saving a book (after we edit it).

fragment_edit_book.xml

We'll start by editing fragment_edit_book.xml. We want to wrap our existing TableLayout in a FrameLayout, like so...

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="book"
            type="com.aboutobjects.curriculum.readinglist.model.EditableBook" />
    </data>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TableLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
...
        </TableLayout>
    </FrameLayout>
</layout>

The easiest way to do that is to add the FrameLayout in (match_parent for height and width), then cut-n-paste the TableLayout into it. The IDE will auto-adjust the formatting.

While we are here, add an id to the TableLayout

android:id="@+id/details"

Inside the FrameLayout, but after the TableLayout, let's add a wait message.

    </TableLayout>

    <LinearLayout
        android:id="@+id/please_wait"
        android:orientation="vertical"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            style="@style/MyTitle"
            android:text="Please wait..."
            android:gravity="center"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

        <ProgressBar
            style="?android:progressBarStyleLarge"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center" />
    </LinearLayout>
</FrameLayout>

We are just creating a standalone layout that shows a text message and a progress bar. The progress bar is getting its' style from the Android framework.

Use the IDE Quick Fix to Extract string resource and name it wait_title.

We've edited the XML, so redeploy to regenerate the binding classes.

EditBookFragment

Open your EditBookFragment.

We only want to show the new pleaseWait layout when we are saving. In your onCreateView let's hide it temporarily

binding.pleaseWait.apply {
    visibility = View.GONE
    alpha = 0f
}

Then, in our onOptionsItemSelected, we will show it (by changing the alpha) over the course of... how about 3 seconds? Then, if there is an error, we will gradually hide it (again, change the alpha) a bit quicker... say, 1 second?

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    return when(item.itemId) {
        R.id.action_save -> {
            binding.book?.let { editableBook ->
                bookListService?.let { service ->
                    binding.pleaseWait.apply {
                        visibility = View.VISIBLE
                        animate()
                            .alpha(1f)
                            .setDuration(3000)
                            .setListener(null)
                    }
                    service.edit(
                        source = editableBook.source,
                        edited = editableBook.edited()
                    )
                        .subscribeOn(Schedulers.newThread())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribeBy(
                            onComplete = {
                                // It's updated, so we are done
                                activity?.supportFragmentManager?.popBackStack()
                            },
                            onError = { t ->
                                t.message?.let { msg ->
                                    binding.pleaseWait.animate()
                                        .alpha(0f)
                                        .setDuration(1000)
                                        .setListener(object: AnimatorListenerAdapter() {
                                            override fun onAnimationEnd(animation: Animator?) {
                                                binding.pleaseWait.visibility = View.GONE
                                                Snackbar.make(binding.root, msg, Snackbar.LENGTH_LONG)
                                                    .show()
                                            }
                                        })
                                }
                            }
                        )
                }
            }
            true
        }
        else -> super.onOptionsItemSelected(item)
    }
}

If you deploy your application, you are unlikely to see the change.

You can temporarily test it by having BookListService.edit immediately return error(IllegalArgumentException("TESTING"))

Please Wait

Why don't we change the alpha back in the onComplete callback?

Since we are immediately popping the backstack, it seemed an unnecessary delay for the end user. This entire Fragment is about to go away. In the case of the onError, the Fragment stays on the screen with the error message in the Snackbar - so having the progress indicator go away gracefully is a nice touch.

More Information

For more information on Animation, please see the Android Developer site.

Clone this wiki locally