Hello good programmers of stack overflow! I've spent a good week with this problem and am now very desperate for a solution.
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:
As you've probably guessed,
FragmentNumeric
shows child fragments FragmentOne
, FragmentTwo
, and FragmentThree
.
FragmentAlpha
shows child fragments FragmentA
, FragmentB
, and FragmentC
.
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.
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>
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()
}
}
}
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()
}
}
}
}
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?
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.
Both the parent and child fragment views are using xFraction property. The key is to suppress the childs animation when the parent is animating.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With