/ android

CircularReveal your Activity and Fragment Part 1

When you think material design you might think elevation, cardviews, flashy colors or whatever. These characteristics are only a tiny part of the Material Design arsenal. This documentation should be a source of inspiration for any developer, UX, UI, PO...

Material is full of energy

Before Material Design every mock-up was static. In the best case UI delivery was an image of what you was supposed to implement and in the worst case it was a powerpoint... Time to time we developers made proposals to give life back to our interfaces with animations. At that time, back in 2014, I was often told things like: "Oh yes it can move/rotate if you want but the cheapest is best".
Another way to say that back in 2014 nobody cares about animations and responsiveness, it feels just as if smartphone interfaces were stuck in the 90's.

Once you want to free your mind about a concept of harmony and music being correct, you can do whatever you want.
Giovanni Giorgio

Motion and interface responsiveness in material design are based on physical behaviors. Each component has a potential energy, when you interact it releases its energy.
When you understand what you should do with that energy, use it to make sense and give your interface the attention it deserves.

Basics: Reveal your potential

My point of view is that a symbol of this energy being released is the radial transformation.

Radial transformation should be used on circular surfaces that morph into rectangular surfaces, or for creating new surfaces from the point of input.
Material Design

So, use it but do not abuse it by trying to reveal anything 😉

Make it Simple

Creating a radial animation is easily achieved by using ViewAnimationUtils#createCircularReveal method by following official documentation here.
What you can see below is a basic implementation of a Kotlin function creating a circular reveal Animator:

private fun getCircularAnimator(targetView: View, sourceX: Int, sourceY: Int, speed: Long): Animator {
        val finalRadius = Math.hypot(targetView.width.toDouble(), targetView.height.toDouble()).toFloat()
        return ViewAnimationUtils.createCircularReveal(targetView, sourceX, sourceY, 0f, finalRadius).apply {
            interpolator = AccelerateDecelerateInterpolator()
            duration = speed
        }
    }

Let's see how it works:

  • targetView is the view that should be revealed
  • sourceX and sourceY stands for the radial effect's start point coordinates
  • There is also speed because I want to control it (300ms by default)
  • Math.hypot(Double, Double) returns the final radius (sqrt(x² +y²)) of the reveal circle to fill the targeted view
    At the end a simple call to Animator#start()
    will do the trick.
    view_reveal

Tip: Hide-and-seek

When you want to reveal a hidden view it only works if visibility is set to INVISIBLE and not GONE. If it is GONE your animation will not work. Before you call Animator#start() set visibility of your view to VISIBLE and we are good to go.

Fragment Circular Reveal

Revealing a view or revealing a Fragment should have almost the same complexity you might ask? Well, it is almost as easy. You have already noticed circular animation is applying to a targetView, now our target view is a fragment layout. We will need some extra information, start coordinates for instance, to achieve this animation.

Fragment's Birth

To start our fragment, we will use StarterPattern with a companion object. The fragment will be started by RevealActivity which contains a FAB button named show_fragment_fab.
revealed_activity-1

The Following code is really basic but it is just here to help us connecting the dots.

class RevealActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_circled)
        show_fragment_fab.setOnClickListener { view ->
            fragmentManager.beginTransaction()
                    .replace(R.id.fragment_container, RevealFragment.newInstance(sourceView = view))
                    .commit()
    }
}

class RevealFragment : Fragment() {

    companion object {
        fun newInstance(sourceView: View? = null) = RevealFragment().apply {
            arguments = Bundle()
            RevealCircleAnimatorHelper.addBundleValues(arguments, sourceView)
        }
    }
}

class RevealCircleAnimatorHelper {

    companion object {
         fun addBundleValues(arguments: Bundle, sourceView: View?) {
            if (sourceView != null) {
                return arguments.putBundle(EXTRAS, Bundle().apply {
                    putFloat(SOURCE_X, sourceView.x + sourceView.width / 2)
                    putFloat(SOURCE_Y, sourceView.y + sourceView.height / 2)
                })
            }
        }
}

RevealCircleAnimatorHelper#addBundleValues(Bundle, View) saves previously clicked view's center coordinates in a bundle. It will help us later to know where to start revealing the animation.

Fragment's Point of View

Start Point

A good start point is essential as material documentation tell it explicitly

Material is full of energy. It quickly responds to user input precisely where the user triggers it.
Material Design

bad good
reveal_bad reveal_good

Rest assured users will see if your animation does not start from the right place. It is a bit like if a magician failed his performance. Failed animations give a poor image of your interface and no matter if your functionality is life changing or not it will have a lower impact on your user.

Do. Or do not. There is no try.
-Yoda-

You rather should not do any animation than one which does not work perfectly.

In our case Reveal effect will start from the center of clicked view. You could use View#setOnTouchListener (View.OnTouchListener) instead if you want to make reveal start from user's input. It only depends on how you see action's source, we have two possible philosophies:

  • User's finger is the trigger
  • Button is the trigger

I choose the second one because I see a button like a switch, you click no matter where on it, it turns lights on.

Bring it to life

Now that our fragment is set up and displaying we can create our reveal animation by calling RevealCircleAnimatorHelper#create(Fragment, View) which will extract previously saved values.

class RevealFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val root = inflater!!.inflate(R.layout.dummy_fragment, container, false)
        RevealCircleAnimatorHelper
                .create(this, container)
                .start(root)
        return root
    }

RevealCircleAnimatorHelper's constructor will transpose coordinates of triggered source view in fragment's referential. We need to do this because (0, 0) origin positions and sizes might be different in fragment and source activity.
Screenshot_1513088400-1
Thanks to source and target views we can find new coordinates in fragment's referential.

    constructor(fragment: Fragment, container: ViewGroup? = null) {
        if (extractBundleValues(fragment.arguments)) {
            fragment.arguments.remove(EXTRAS)
            container?.let {
                if (container.x < mSourceX) mSourceX -= container.x else mSourceX = 0f
                if (container.y < mSourceY) mSourceY -= container.y else mSourceY = 0f
            }
        }
    }

3,2,1... Ignition!

It is not a rocket, more a little bike :) but still, we are ready to connect all the wires to make it reveal ! We can now use previously defined RevealCircleAnimatorHelper#getCircularAnimator and start the animation with RevealCircleAnimatorHelper#start(View).

        fun start(rootLayout: View) {
        if (mSourceX >= 0 && mSourceY >= 0) {
            rootLayout.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
                override fun onLayoutChange(rootLayout: View?, p1: Int, p2: Int, p3: Int, p4: Int, p5: Int, p6: Int, p7: Int, p8: Int) {
                    rootLayout?.let {
                        getCircularAnimator(it, mSourceX.toInt(), mSourceY.toInt(), CIRCULAR_SPEED).start()
                    }
                    rootLayout?.removeOnLayoutChangeListener(this)
                }
            })
        }
    }

We had an OnLayoutChangeListener which is an update listener for a specific View. With this listener we will trigger the animation only when the specific view goes through a layout pass and then we can start our revealing.
reveal_fragment

Conclusion

It was not that hard to get our view and fragment revealed. Still, as usual with animations it is always small tricks to make it clean and reusable. I think we reached that point with the solution proposed here.

Do not forget that reveal animations are great for circular shapes morphing to a square one and only in that case.

We will see in the next part how to reveal an activity with some color morphing to make a perfect transition between two activities.

Thanks a lot for reading, see you next time for part 2 !

All source code can be found on GitHub