[go: nahoru, domu]

Complete transitions if the container is not laid out

If the container has not be laid out, beginDelayedTransition() is a
no-op and since the transition is never started, it never ends so the
SpecialEffectController operation never gets completed.

In the case where the container has not been laid out, we should complete
the operation immediately.

Unlike transitions, Animations run whether they have been laided out or
not and they should continue to do so.

RelNote: "Child fragments with transitions will now properly reach
RESUMED"
Test: enterExitChildTransitions
Bug: 180825150

Change-Id: Ic11e655bd4ed206485c2762a1af4e5bc7aa4cf9c
(cherry picked from commit 081f27aa164ea9566d198f05166130cc27a52bfe)
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt
index 1ab4cad..fc95ca3 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt
@@ -21,9 +21,14 @@
 import android.transition.Transition
 import android.transition.TransitionSet
 import android.view.View
+import android.widget.TextView
+import androidx.annotation.LayoutRes
 import androidx.core.app.SharedElementCallback
 import androidx.fragment.app.test.FragmentTestActivity
 import androidx.fragment.test.R
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
 import androidx.test.core.app.ActivityScenario
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
@@ -123,6 +128,55 @@
         assertThat(onBackStackChangedTimes).isEqualTo(4)
     }
 
+    @Test
+    fun enterExitChildTransitions() {
+        // enter transition
+        val fragment = TransitionFragment()
+
+        fragmentManager.beginTransaction()
+            .setReorderingAllowed(reorderingAllowed)
+            .replace(R.id.fragmentContainer, fragment)
+            .addToBackStack(null)
+            .commit()
+        activityRule.waitForExecution()
+
+        fragment.waitForTransition()
+        val view = activityRule.activity.findViewById<TextView>(R.id.text1)
+        fragment.enterTransition.verifyAndClearTransition {
+            enteringViews += view
+        }
+        verifyNoOtherTransitions(fragment)
+
+        lateinit var parent: ParentTransitionFragment
+
+        activityRule.runOnUiThread {
+            parent = ParentTransitionFragment()
+        }
+
+        fragmentManager.beginTransaction()
+            .setReorderingAllowed(reorderingAllowed)
+            .replace(R.id.fragmentContainer, parent)
+            .addToBackStack(null)
+            .commit()
+        activityRule.waitForExecution()
+
+        fragment.waitForTransition()
+        fragment.exitTransition.verifyAndClearTransition {
+            exitingViews += view
+        }
+        verifyNoOtherTransitions(fragment)
+
+        parent.waitForTransition()
+        val childView = activityRule.activity.findViewById<TextView>(R.id.text1)
+        parent.enterTransition.verifyAndClearTransition {
+            enteringViews += childView
+        }
+        verifyNoOtherTransitions(parent)
+
+        assertThat(parent.isResumed).isTrue()
+        assertThat(parent.child.isResumed).isTrue()
+    }
+
     // Test removing a Fragment with a Transition and adding it back before the Transition
     // finishes is handled correctly.
     @Test
@@ -1564,6 +1618,24 @@
         }
     }
 
+    class ParentTransitionFragment(
+        @LayoutRes contentLayoutId: Int = R.layout.fragment_container_view
+    ) : TransitionFragment(contentLayoutId) {
+        val child = TransitionFragment()
+        init {
+            lifecycle.addObserver(object : LifecycleEventObserver {
+                override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+                    if (event == Lifecycle.Event.ON_START) {
+                        childFragmentManager.beginTransaction()
+                            .add(R.id.fragment_container_view, child)
+                            .setReorderingAllowed(true)
+                            .commit()
+                    }
+                }
+            })
+        }
+    }
+
     companion object {
         @JvmStatic
         @Parameterized.Parameters(name = "ordering={0}, stateManager={1}")
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.java b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.java
index 0008abd..aa72c76 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.java
@@ -621,18 +621,35 @@
             boolean involvedInSharedElementTransition = sharedElementTransition != null
                     && (operation == firstOut || operation == lastIn);
             if (transition != null || involvedInSharedElementTransition) {
-                transitionImpl.setListenerForTransitionEnd(
-                        transitionInfo.getOperation().getFragment(),
-                        mergedTransition,
-                        transitionInfo.getSignal(),
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                transitionInfo.completeSpecialEffect();
-                            }
-                        });
+                // If the container has never been laid out, transitions will not start so
+                // so lets instantly complete them.
+                if (!ViewCompat.isLaidOut(getContainer())) {
+                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                        Log.v(FragmentManager.TAG,
+                                "SpecialEffectsController: Container " + getContainer()
+                                        + " has not been laid out. Completing operation "
+                                        + operation);
+                    }
+                    transitionInfo.completeSpecialEffect();
+                } else {
+                    transitionImpl.setListenerForTransitionEnd(
+                            transitionInfo.getOperation().getFragment(),
+                            mergedTransition,
+                            transitionInfo.getSignal(),
+                            new Runnable() {
+                                @Override
+                                public void run() {
+                                    transitionInfo.completeSpecialEffect();
+                                }
+                            });
+                }
             }
         }
+        // Transitions won't run if the container isn't laid out so
+        // we can return early here to avoid doing unnecessary work.
+        if (!ViewCompat.isLaidOut(getContainer())) {
+            return startedTransitions;
+        }
         // First, hide all of the entering views so they're in
         // the correct initial state
         FragmentTransition.setViewVisibility(enteringViews, View.INVISIBLE);