Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nested fragments transitioning incorrectly

Hello good programmers of stack overflow! I've spent a good week with this problem and am now very desperate for a solution.


The scenario

I'm using android.app.Fragment's not to be confused with the support fragments.

I have 6 child fragments named:

  • FragmentOne
  • FragmentTwo
  • FragmentThree
  • FragmentA
  • FragmentB
  • FragmentC

I have 2 parent fragments named:

  • FragmentNumeric
  • FragmentAlpha

I have 1 activity named:

  • MainActivity

They behave in the following:

  • Child fragments are fragments that only show a view, they do not show nor contain fragments.
  • Parent fragments fill their entire view with a single child fragment. They're able to replace the child fragment with other child fragments.
  • The activity fills most of its view with a parent fragment. It can replace it with other parent fragments. So at any one time the screen has just a single child fragment showing.

As you've probably guessed,

FragmentNumeric shows child fragments FragmentOne, FragmentTwo, and FragmentThree.

FragmentAlpha shows child fragments FragmentA, FragmentB, and FragmentC.


The problem

I'm trying to transition/animate parent and child fragments. The child fragments transition smoothly and as expected. However when I transition to a new parent fragment, it looks terrible. The child fragment looks like it runs an independent transition from its parent fragment. And the child fragment looks like it is removed from the parent fragment as well. A gif of it can be viewed here https://imgur.com/kOAotvk. Notice what happens when I click Show Alpha.

The closest question & answers I could find are here: Nested fragments disappear during transition animation however all of the answers are unsatisfying hacks.


Animator XML Files

I have the following animator effects (duration is long for testing purposes):

fragment_enter.xml

<?xml version="1.0" encoding="utf-8"?>
<set>
    <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="4000"
        android:interpolator="@android:anim/linear_interpolator"
        android:propertyName="xFraction"
        android:valueFrom="1.0"
        android:valueTo="0" />
</set>

fragment_exit.xml

<?xml version="1.0" encoding="utf-8"?>
<set>
    <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="4000"
        android:interpolator="@android:anim/linear_interpolator"
        android:propertyName="xFraction"
        android:valueFrom="0"
        android:valueTo="-1.0" />
</set>

fragment_pop.xml

<?xml version="1.0" encoding="utf-8"?>
<set>
    <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="4000"
        android:interpolator="@android:anim/linear_interpolator"
        android:propertyName="xFraction"
        android:valueFrom="0"
        android:valueTo="1.0" />
</set>

fragment_push.xml

<?xml version="1.0" encoding="utf-8"?>
<set>
    <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="4000"
        android:interpolator="@android:anim/linear_interpolator"
        android:propertyName="xFraction"
        android:valueFrom="-1.0"
        android:valueTo="0" />
</set>

fragment_nothing.xml

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

MainActivity.kt

Things to consider: The first parent fragment, FragmentNumeric, doesn't have enter effects so its always ready with the activity and doesn't have exit effects because nothing is exiting. I'm also using FragmentTransaction#add with it where as FragmentAlpha is using FragmentTransaction#replace

class MainActivity : AppCompatActivity {

    fun showFragmentNumeric(){
        this.fragmentManager.beginTransaction()
                .setCustomAnimations(R.animator.fragment_nothing,
                                     R.animator.fragment_nothing,
                                     R.animator.fragment_push,
                                     R.animator.fragment_pop)
                .add(this.contentId, FragmentNumeric(), "FragmentNumeric")
                .addToBackStack("FragmentNumeric")
                .commit()
}

    fun showFragmentAlpha(){
        this.fragmentManager.beginTransaction()
                .setCustomAnimations(R.animator.fragment_enter,
                                     R.animator.fragment_exit,
                                     R.animator.fragment_push,
                                     R.animator.fragment_pop)
                .replace(this.contentId, FragmentAlpha(), "FragmentAlpha")
                .addToBackStack("FragmentAlpha")
                .commit()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (savedInstanceState == null) {
            showFragmentNumeric()
        }
    }
}

FragmentNumeric

Does the same thing as the activity in terms of quickly showing its first child fragment.

class FragmentNumeric : Fragment {

    fun showFragmentOne(){
            this.childFragmentManager.beginTransaction()
                    .setCustomAnimations(R.animator.fragment_nothing,
                                         R.animator.fragment_nothing,
                                         R.animator.fragment_push,
                                         R.animator.fragment_pop)
                    .add(this.contentId, FragmentOne(), "FragmentOne")
                    .addToBackStack("FragmentOne")
                    .commit()
    }

    fun showFragmentTwo(){
        this.childFragmentManager.beginTransaction()
                .setCustomAnimations(R.animator.fragment_enter,
                                     R.animator.fragment_exit,
                                     R.animator.fragment_push,
                                     R.animator.fragment_pop)
                .replace(this.contentId, FragmentTwo(), "FragmentTwo")
                .addToBackStack("FragmentTwo")
                .commit()
    }


    fun showFragmentThree(){
        this.childFragmentManager.beginTransaction()
                .setCustomAnimations(R.animator.fragment_enter,
                                     R.animator.fragment_exit,
                                     R.animator.fragment_push,
                                     R.animator.fragment_pop)
                .replace(this.contentId, FragmentThree(), "FragmentThree")
                .addToBackStack("FragmentThree")
                .commit()
    }

    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        if (savedInstanceState == null) {
            if (this.childFragmentManager.backStackEntryCount <= 1) {
                showFragmentOne()
            }
        }
    }
}

Other Fragments

FragmentAlpha is following the same pattern as FragmentNumeric, replacing Fragments One, Two and Three with Fragments A, B and C respectively.

Children fragments are just showing the following XML view and setting its text and button click listener dynamically to either call a function from the parent fragment or activity.

view_child_example.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/background"
    android:clickable="true"
    android:focusable="true"
    android:orientation="vertical">

    <TextView
        android:id="@+id/view_child_example_header"
        style="@style/Header"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />


    <Button
        android:id="@+id/view_child_example_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"  />
</LinearLayout>

Using dagger and some contracts I have the child fragments callback to their parent fragments and hosting activities by doing something like the below:

FragmentOne sets the button click listener to do:

(parentFragment as FragmentNumeric).showFragmentTwo()

FragmentTwo sets the button click listener to do:

(parentFragment as FragmentNumeric).showFragmentThree()

FragmentThree is different, it will set the click listener to do:

(activity as MainActivity).showFragmentAlpha()

Does anyone have a solution for this problem?


Update 1

I've added an example project as requested: https://github.com/zafrani/NestedFragmentTransitions

A difference in it and that from the one in my original video is the parent fragment no longer uses a view with the xFraction property. So it looks like the enter animation doesn't have that overlapping effect anymore. It still however does remove the child fragment from the parent and animate them side by side. After the animation completes, Fragment Three is replaces with Fragment A instantly.

Update 2

Both the parent and child fragment views are using xFraction property. The key is to suppress the childs animation when the parent is animating.

like image 873
zafrani Avatar asked Oct 21 '17 01:10

zafrani


1 Answers

I think I've found a way to solve this using Fragment#onCreateAnimator. A gif of the transition can be viewed here: https://imgur.com/94AvrW4.

I made a PR for testing, so far it's working as I expect and surviving configuration changes and supporting the back button. Here is the link https://github.com/zafrani/NestedFragmentTransitions/pull/1/files#diff-c120dd82b93c862b01c2548bdcafcb20R25

The BaseFragment for both the Parent and Child fragments is doing this for onCreateAnimator()

override fun onCreateAnimator(transit: Int, enter: Boolean, nextAnim: Int): Animator {
    if (isConfigChange) {
        resetStates()
        return nothingAnim()
    }

    if (parentFragment is ParentFragment) {
        if ((parentFragment as BaseFragment).isPopping) {
            return nothingAnim()
        }
    }

    if (parentFragment != null && parentFragment.isRemoving) {
        return nothingAnim()
    }

    if (enter) {
        if (isPopping) {
            resetStates()
            return pushAnim()
        }
        if (isSuppressing) {
            resetStates()
            return nothingAnim()
        }
        return enterAnim()
    }

    if (isPopping) {
        resetStates()
        return popAnim()
    }

    if (isSuppressing) {
        resetStates()
        return nothingAnim()
    }

    return exitAnim()
}

The booleans are being set in different scenarios that are easier to see in the PR.

The animation functions are:

private fun enterAnim(): Animator { 
        return AnimatorInflater.loadAnimator(activity, R.animator.fragment_enter)
    }

    private fun exitAnim(): Animator { 
        return AnimatorInflater.loadAnimator(activity, R.animator.fragment_exit)
    }

    private fun pushAnim(): Animator { 
        return AnimatorInflater.loadAnimator(activity, R.animator.fragment_push)
    }

    private fun popAnim(): Animator { 
        return AnimatorInflater.loadAnimator(activity, R.animator.fragment_pop)
    }

    private fun nothingAnim(): Animator { 
        return AnimatorInflater.loadAnimator(activity, R.animator.fragment_nothing)
    }

Will leave the question open incase someone finds a better way.

like image 194
zafrani Avatar answered Oct 20 '22 01:10

zafrani