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:
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.
See Question&Answers more detail:
os