/ android

CircularReveal your Activity and Fragment Part 2

In our last episode

"The FIRST ORDER reigns. Having decimated the peaceful Republic..." Oups, wrong story... Our journey started with Part 1 I strongly recommend to follow the path and read it before going further here.

Activity Circular Reveal

Keep it DRY

Fragment reveal was pretty simple but can we do the same with activities? Of course and exactly like we did before with fragments. As usual now, we have a MainActivity with a FAB which starts RevealActivity when pushing it.

Then we will call the same functions as for fragments, like in Part 1.

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        fab.setOnClickListener { view ->
            startActivity(RevealActivity.newIntent(this, view))
        }
    }
}

class RevealActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_circled)
        RevealCircleAnimatorHelper
                .create(this)
                .start(root)
    }
    companion object {
        fun newIntent(context: Context, sourceView: View? = null): Intent {
            return Intent(context, RevealActivity::class.java).also {
                RevealCircleAnimatorHelper.addBundleValues(it, sourceView)
            }
        }
    }
}

It works almost the same way. We call RevealCircleAnimatorHelper.create(this).start(root) from the newly launched activity which will then retrieve previously bundled coordinates in newInstance with RevealCircleAnimatorHelper#addBundleValues(Bundle, View). The only difference with handling fragment launch is that the companion method RevealCircleAnimatorHelper#addBundleValues(Intent, View) takes an Intent as first parameter instead of a bundle.
reveal_activity_bad

Switching From dark side to light side

You noticed that we have a dark background. We need to fix activity's theme by setting the window to translucent and the background transparent to be able to see the RevealActivity over MainActivity .

    <style name="AppTheme.Circled" parent="AppTheme.NoActionBar">
        <item name="android:windowIsTranslucent">true</item>
        <item name="colorPrimary">@color/colorAccent</item>
        <item name="android:windowBackground">@android:color/transparent</item>
    </style>

reveal_activity

It's a double Rainbow!

As you can see we still have a problem with colors when switching from an activity to another. Often both activities have their backgrounds white and you won't see clearly the transition. But what if we play a bit with colors to bridge between my two activies?

We are going to make our revealed activity's background tinted. It will start with FAB color which will be faded smoothly to a target color. To be able to change background colors on demand I will use ValueAnimator.ofArgb(Int, Int) method to get this one done.

        private fun getColorCrossFadeAnimator(targetView: View, @ColorInt fromColor: Int, @ColorInt targetColor: Int, speed: Long): ValueAnimator {
        targetView.setBackgroundColor(fromColor)
        return ValueAnimator.ofArgb(fromColor, targetColor).apply {
            interpolator = AccelerateDecelerateInterpolator()
            duration = speed
            // Set new color to background smoothly
            addUpdateListener { animation ->
                targetView.setBackgroundColor(animation.animatedValue as Int)
            }
            // if canceled or ended set final color
            addListener(object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animation: Animator) {
                    targetView.setBackgroundColor(targetColor)
                }
                
                override fun onAnimationCancel(animation: Animator) {
                    targetView.setBackgroundColor(targetColor)
                }
            })
        }
    }

You can test this method simply by connecting a FAB button of this activity to a getColorCrossFadeAnimator(View, @ColorInt Int, @ColorInt Int, Long) function.
crossfadeactivity

The Best Of Both Worlds

Hannah Montana would be proud of us : we are going to mix everything now and make it all together. Let's play a little with an AnimatorSet to play sequentially our animators because we want the fade to occur just after the reveal animation ended.

      private fun startCircularAnimation(rootLayout: View?, @ColorInt fromColor: Int?, @ColorInt targetColor: Int?) {
        rootLayout?.let {
            val circularReveal = getCircularAnimator(it, mSourceX.toInt(), mSourceY.toInt(), CIRCULAR_SPEED)
            val animator = AnimatorSet()
            if (fromColor != null && targetColor != null) {
                val fadeAnimator = getColorCrossFadeAnimator(it, fromColor, targetColor, CIRCULAR_SPEED)
                animator.play(circularReveal).before(fadeAnimator)
            } else {
                animator.play(circularReveal)
            }
            animator.start()
        }
    }

The onLayoutChangeListener will change a bit from this Part 1 : 3,2,1... Ignition to make it call RevealCircleAnimatorHelper#startCircularAnimation(View, @ColorInt Int, @ColorInt Int, Long) instead of RevealCircleAnimatorHelper#getCircularAnimator.

 fun start(rootLayout: View, @ColorInt fromColor: Int? = null, @ColorInt targetColor: Int? = null) {
        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) {
                    startCircularAnimation(rootLayout, fromColor, targetColor)
                    rootLayout?.removeOnLayoutChangeListener(this)
                }
            })
        }
    }

You will notice that two colors appear as function's parameter. However, colors are optional and revealing Fragment or Activity can define colors they want to transition from and to.

class RevealActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_circled)
        RevealCircleAnimatorHelper
                .create(this)
                .start(root, getThemeColor(R.attr.colorAccent), getColor(R.color.background_default, theme))
    }
    
    companion object {
    
        fun newIntent(context: Context, sourceView: View? = null): Intent {
            return Intent(context, RevealActivity::class.java).also {
                RevealCircleAnimatorHelper.addBundleValues(it, sourceView)
            }
        }
    }

reveal_final

End of the Road

What a journey! We started from something quite easy to do with SDK's already defined animations to and with a complex system able to reveal anything, anywhere.
It is a simple solution to make a smooth transition from a page to another without applying the overused fade or translate transition.
Dealing with animations is always the same: tricks, little time-consuming mistakes, quick first results, not that quick good enough result and a lot of proudness when you achieve your goal.

Thanks a lot for reading, see you next time ;)

All source code can be found on GitHub