My FAB brings all the boys to the yard, damn right

Antonis Tsagaris
Sep 15, 2016 · 9 min read

(hello there, BulletTooth [rest of name redacted for privacy reasons. I just left this here to prove a point)

When Google announced Android 5.0 Lollipop, they showed a video of a music player that wowed the crowds and became the Holy Grail of Material Design-obsessed people, myself included.

Skip to 30 seconds into the video and see for yourself:

People expected the Google Play Music app update that would bring all this smoothness and creaminess to their devices but it never arrived. However, the curved motion of the Floating Action Button that, after the translation, expands to reveal a new panel of controls inspired so many people that a lot of app designers and developers went ahead and implemented it (for a genius example, go ahead and download Morning Routine: Alarm Clock from the Play Store and create an alarm).

<rant>

In true Google fashion, Google gave us the guidelines for Material Design but didn’t give us a lot of the building blocks that they wanted us to use. There was no FAB to be found in the framework: you had to design it yourself. “It’s not that hard”, Google engineers laughed, “it’s just an icon inside a circle. You can do it yourself, right?”

Which is correct but also misses the point entirely. Yeah, I can do it myself. I can place an icon in the center of a circle. Of course, that means that I’ll also have to create a custom view if I want to stay DRY. And also, I’ll have to use your completely convoluted way of getting an outline for the view to add the circular shadow, unless I want it to be rectangular.

Sounds like fun.

</rant>

Anywayzzz. They finally added a FAB view in the Android Design Support Library, as most of you know, so the rant may be outdated but I’ve really wanted to rant about it for a long time.

So, back to the subject of this post. I think I’ll finally get there.

Some days ago I decided to create an app as a weekend project. The app is called YourBook and I haven’t released it yet (be honest, which weekend project of yours took an actual weekend?) but what I wanted to do was have a FAB button with a “plus” icon on it that, when tapped, would expand to reveal a card on which you could create a new book.

You can see the animation in action here:

The video ranked second on that day’s posts on materialup and some people asked me for a tutorial, hence this post.

The animation is a combination of a shared element transition, a circular reveal (and hide) animation and some other tricks I had to employ to make the animation cooler.

Let’s go through it:

The first step is creating a shared element transition between the two activities.

The shared element is, obviously, the FAB. To create the transition, we have to set up some things. Firstly, we have to make the motion circular, otherwise, it the FAB will move in a straight line, which won’t look as snazzy. Which isn’t acceptable, as snazziness was a requirement here. We also have to make sure that the two activities have been set up to support the transitions, otherwise nothing awesome will happen.

You can see the code for this below. This is inside a method called setupActivityAnimations(), which I call right before .setContentView()

public void setupActivityAnimations() {

if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

TransitionSet set = new TransitionSet();
TransitionInflater inflater = TransitionInflater.from(this);
Transition transition = inflater
.inflateTransition(R.transition.arc_trans);

set.addTransition(new ChangeImageTransform());
set.addTransition(transition);
set.setOrdering(TransitionSet.ORDERING_TOGETHER);
set.setDuration(600);
set.setInterpolator(new AccelerateDecelerateInterpolator());

Transition slide = new Slide();
slide.excludeTarget(android.R.id.statusBarBackground, true);
slide.excludeTarget(android.R.id.navigationBarBackground, true);
getWindow().requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS);
getWindow().setEnterTransition(slide);
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
getWindow().setAllowEnterTransitionOverlap(true);
getWindow().setSharedElementEnterTransition(set);
getWindow().setSharedElementReturnTransition(set);
getWindow().setSharedElementReenterTransition(set);
getWindow().setSharedElementExitTransition(set);
getWindow().setReenterTransition(slide);
getWindow().setExitTransition(slide);
Window w = getWindow();
w.setFlags(
WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
}
}

In this case, the API level check is redundant, since the app only works on Lollipop+, but most people doing this won’t be as lucky as me.

Whew. There’s a ton of stuff going on in there! First, we create the transition set that will contain the curved transition. We then inflate an .xml animation using TransitionInflater and add it to the set, which we keep for later use. Because I am not motherfuckin’ Google, I will share the code for the .xml animation file and won’t tell you “it’s easy, do it yourself” (yup, still holding that grudge)

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

<arcMotion
android:maximumAngle="90"
android:minimumHorizontalAngle="35"
android:minimumVerticalAngle="0" />

</changeBounds>

Keep in mind that the correct folder in your project to place this file is resources/transition. You’re welcome!

After this, you will see that I create another Transition, which is the activity transition, ie. it has nothing to do with the FAB, it just makes the rest of the elements in both activities slide when moving between them (for example, look at the grid of books in the first activity and how it slides away). After excluding the status bar and the navigation bar from the transition (so that they won’t transition too, that would look janky) I request that activity transitions are allowed with

getWindow().requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS);

and set the animation by using .setEnterTransition(), .setReenterTransition() and .setExitTransition(), with the animation as an argument. That takes care of the slide.

Afterwards, you will see a sequence of .getWindow() calls and some setters called on the Window object, which set the shared element transition as the transition we inflated earlier (remember that TransitionSet object? Yeah, that one!)

The second step is making sure that it is the FAB that is the shared element between the two activities

The way we do this is by going into the .xml layout file of both activities and, on both FAB views, set the attribute transitionName to be exactly the same

<android.support.design.widget.FloatingActionButton
android:transitionName="fab_transition"
android:id="@+id/fab"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_margin="16dp"

/>

The above code is from the FAB view in the MainActivity. The FAB view in the second activity also has to have the attribute android:transitionName = “fab_transition”. Be really careful with this, otherwise you’ll be pulling your hair and wonder why it doesn’t work before succumbing to alcoholism and living a life in the gutter. Do you really want to be the inspiration for a Steely Dan song? Thought not.

The third step is launching the second activity by giving it an ActivityOptions object as a bundle

fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, CreateBookActivity.class);

ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(MainActivity.this
, fab, "fab_transition");

startActivity(intent, options.toBundle());
}
});

Just to make some things clear, fab is the FAB view referenced in code, CreateBookActivity is the second activity that’s launched after tapping the FAB the the fab_transition string argument in .makeSceneTransitionAnimation() has to be the same string that you used in the .xml layout file, in the FAB view (android:transitionName=”fab_transition”).

And you’re done with the Activity transition and the Shared Element transition.

Part 4 is making the animation as snazzy as can be!

Now, you have to reveal the card. The duration for the curved transition is 600ms (in the setupActivityAnimations() method above, you will notice that I set the duration of the animation to 600ms), so that card has to start getting revealed just as the FAB settles into position in the second activity. For that reason, in the second activity, I delay the reveal animation by 600ms. To do that, I do the following

fab.postDelayed(new Runnable() {
@Override
public void run() {

revealView(infoBookLayout);
}
}, 600);

infoBookLayout is, of course, the card that gets revealed.

Notice that I use one of my favorite tricks! The View class has a .post() and .postDelayed() method which is inherited by its subclasses (in this case, FloatingActionButton) and which is executed after the view has been measured. Keep in mind that this is an awesome way to measure a view’s dimension’s programmatically too, because if you try to do it in onCreate(), you will get a result before the view is measured and the result of view.getWidth() and view.getHeight() will be 0 (zero)

Now, for the revealView() method

public void revealView(View view) {

int centerY = view.getMeasuredHeight() / 2;
int centerX = view.getMeasuredWidth() / 2;

Animator animator = ViewAnimationUtils.createCircularReveal(view,
centerX, centerY, 0, view.getWidth());

animator.setDuration(400);
view.setVisibility(View.VISIBLE);
animator.setStartDelay(0);
animator.start();

}

I won’t go too deep into this because it is code that Google has given us (for once) but here is a link about it!

When we want to hide the view, we run the hideView() method

public void hideView(final View view) {

int centerY = view.getMeasuredHeight() / 2;
int centerX = view.getMeasuredWidth() / 2;

Animator animator = ViewAnimationUtils.createCircularReveal(view,
centerX, centerY, view.getHeight(), Utilities.dp2px(32, this));

animator.setDuration(800);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
view.setVisibility(View.INVISIBLE);
}
});
animator.start();

}

Notice that, for the last argument in ViewAnimationUtils.createCircularReveal, I use Utilities.dp2px(32, this).

An explanation is in order: the createCircularReveal() static method takes the following arguments, in order:

  • the view object,
  • the center in the X-axis (centerX)
  • the center in the Y-axis (centerY),
  • the final revealed view’s largest dimension (the height, in this case, since the view is in portrait) and,
  • as its last argument, the remaining radius of the view after the animation is finished. This is very important!

In our case, we don’t want the view to dissapear completely, and not because we have an issue with Radiohead.

One of the tricks I used was that the card view is not actually expanded from the FAB. Instead, the card sits in front of the FAB (it is higher in elevation). When the card expands, the FAB is hidden but because this is done very fast, it looks like the card is sort of the result of an expanded FAB, which is a trick. No, sorry, it’s not a trick, it’s an illusion.

If the card disappears completely, its contraction over the FAB looks unsightly, if you’re OCD like me, because the shadow of its contraction (because of the card’s elevation) completely ruins the effect, making it obvious that the card is OVER the FAB, not the result of its expansion. So what I did was keep the final radius of the view at 32dp. Why 32dp? Well, because the FAB view has a width and height of 64dp! Nifty, right? This way, no crappy effect-ruining shadow over the FAB, no nothing!

As for the dp2px method, I found it on StackOverflow and I’m not afraid to admit it

public static int dp2px(float dp, Context context) {

if (context != null) {
return (int) (dp
* context.getResources().getDisplayMetrics().density + 0.5f);
} else {

return (int) dp;
}
}

Just use it as is, nobody will know. LOL, fools!

For the final part of my performance, one more thing I did was make the various EditText, TextView and ImageView elements on the card animate to scaleX(0.0f) and scaleY(0.0) when the back arrow (on the top of the card) is tapped so that they disappear by sort of zooming out.

Something like

victim.animate().setStartDelay(200).setDuration(100).scaleX(0.0f).scaleY(0.0f).start();

After that is done, I also make a plus icon visible in the center of the card (the icon is an ImageView that’s actually in the card from the beginning, just with its visibility set to View.INVISIBLE and with an attribute of android:layout_centerInParent=”true”), so that when the card is (partially) hidden (remember, I leave a radius of 32dp visible), it looks like it morphs into the FAB. After that, I call onBackPressed() in the activity and the return transition to the first activity takes care of itself!

Conclusion

Making animation appear effortless is probably more work than you think. Those two seconds or so of animation took me about 4 hours to get exactly as I wanted them. Is it worth it? Or should you spend your time adding, say, more features to your app, watching Shark Tank or chillin’ with some piña coladas?

In my opinion, it is totally worth it. More featureas are not always a good thing, Mark Cuban is a smug mofo and… piña coladas? What are you? Nine?

I have noticed that two things make a mobile application stand out from the rest: smart use of animations (make it feel alive!) and gestures. An app that’s totally static feels dead. An app that only allows the user to interact with taps? Again, dead.

Make sure that your app doesn’t have the horrible stench of death.

Animate more!

PS. for any questions, corrections or piña colada-related hate mail, don’t hesitate to comment or send me an email at sebastian212000@gmail.com. Favorite method of being terrorised: horse heads on my porch. Make sure the rest of the horse is still attached.

Antonis Tsagaris

Written by

Founder @ Looxie (http://looxie.co) Android developer for http://codehousefive.com, author of Android Development for Gifted Primates https://amzn.to/2ApMFwe

More From Medium

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade