05 September 2017     

by Jim Hu, Workday San Francisco

Fragment transitions with RecyclerView items “sliding apart” or “Split”


We wanted to animate an inbox-like feature’s transition from List to Details with Material transitions that follows the Material Design guidelines stated here.

The example provided by the guideline is Google’s Android Inbox App, it has the following characteristics:

  • The selected inbox item and all above it slide out from the top.
  • All items under the selected slides out from the bottom.
  • Detail page slides up uniformly from the bottom
  • Loading should start immediately after click and as soon as transition starts

We will focus on the “Sliding” for now

Note: Some examples are in Kotlin


Transition terms

The transition terms are confusing at first glance, using our example, here’s the 4 transition in layman’s terms, in their actual triggered order:

Opening an item from the List:

  • ExitTransition — List’s transition for List→Detail
  • EnterTransition — Detail’s transition for List → Detail

Clicking back or returning from Detail to List:

  • ReturnTransition — Detail’s transition for Detail → List
  • ReenterTransition — List’s transition for Detail → List

*Reverse animations are also autogenerated for the target’s reverse action (can be overridden by setting them yourselves), i.e.

  • Setting List’s ExitTransition will autogenerate it’s ReenterTransition
  • Setting Detail’s EnterTransition will autogenerate it’s ReturnTransition

In code it’ll look something like this

detailFragment.enterTransition = Slide()
// If you don't want the Fade on the way back 
detailFragment.returnTransition = null 
listFragment.exitTransition = Explode()
//listFragment.reenterTransition is auto generated reversed

Physical limitations elimination

There’s a few subtle but important prerequisites that must be set on the layouts.

RecyclerView:

<android.support.v7.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:transitionGroup="false"
    android:clipChildren="false" />

ItemView Layout:

<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:transitionGroup="true"
    tools:targetApi="lollipop">

Breaker of chains — Transition Groups

By default all views under a parent/ancestor with a background set (even transparent ones) will be automatically deemed a group. If you need to break them up like we here with a RecyclerView as the shared-root-white-backgrounded layout with transparent child Item views. You’ll need to set the layout with the background to transitionGroup=false.

But on the other hand, since the Items are “background-less” themselves, to prevent an out-of-body experience you’ll need to do the opposite and set transitionGroup=true on the Item layouts for all the child views in that Item to move together.

Set the children free! — Remove parent layout boundaries

Items inside RecyclerViews are limited to animating inside their parent’s boundaries, to let the items fly over the AppBar on top, we’ll need to set clipChildren=”false” on all parents/ancestors up the view hierarchy.


Crafting the Transition — The vertical “Split”

Since Android 5.0 there’s 3 available default visibility transitions available: Explode, Fade, Slide, but wait, no Split?!? 😱

Luckily, if you look closely (in one dimensional vision 👀), you’ll realize a Split is actually just a one dimensional Explode! Setting the Explode transition’s epic-center to an horizontal line right under the selected item will let the view explode vertically.

private fun getListFragmentExitTransition(itemView: View): Transition {
      val epicCenterRect = Rect()
      //itemView is the full-width inbox item's view
      itemView.getGlobalVisibleRect(epicCenterRect)
      // Set Epic center to a imaginary horizontal full width line under the clicked item, so the explosion happens vertically away from it
      epicCenterRect.top = epicCenterRect.bottom
      val exitTransition = Explode()
      exitTransition.epicenterCallback = object : Transition.EpicenterCallback() {
          override fun onGetEpicenter(transition: Transition): Rect {
              return epicCenterRect
          }
      }
}

and Voilà!


But the items are not sliding away, but still *exploding *i.e. Items not flying together. Nobody’s inbox should explode (at least not visually..)

Propagation

The explode effect is actually achieved by propagation, which delays view animation according to the view’s distance from the epic-center, i.e. start later if the item’s closer to the epic-center. As we want to just Slide, we disable Propagation by: exitTransition.setPropagation(null)

And~~ now we have the Slide! So smooth~



Additional stuff — Adding FadeOut

You may have probably noticed that we also incorporated “Fade Out” into the transition. Ideally one could just utilize TransitionSets and stuff them together to your heart’s content. But due to a bug, Fragment Exit/Reenter transitions to not work with TransitionSets (Enter/Return works). Hence for whoever that has this problem, we could just inherit the Transition and manipulate the underlying AnimationSet, the following is the full inherited class:

Note: onAppear is for Enter/Reenter, onDisappear is for Exit/Return

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
class ExplodeFadeOut : Explode() {
    init {
        propagation = null
    }

    override fun onAppear(sceneRoot: ViewGroup?, view: View?, startValues: TransitionValues?,
                          endValues: TransitionValues?): Animator {
        val explodeAnimator = super.onAppear(sceneRoot, view, startValues, endValues)
        val fadeInAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 0f, 1f)

        return animatorSet(explodeAnimator, fadeInAnimator)
    }

    override fun onDisappear(sceneRoot: ViewGroup?, view: View?, startValues: TransitionValues?,
                             endValues: TransitionValues?): Animator {
        val explodeAnimator = super.onDisappear(sceneRoot, view, startValues, endValues)
        val fadeOutAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 1f, 0f)

        return animatorSet(explodeAnimator, fadeOutAnimator)
    }

    private fun animatorSet(explodeAnimator: Animator, fadeAnimator: Animator): AnimatorSet {
        val animatorSet = AnimatorSet()
        animatorSet.play(explodeAnimator).with(fadeAnimator)
        return animatorSet
    }
}