Fix focus problems when using optimized fragment transactions.
Bug 33754255
Instead of making Views INVISIBLE immediately after adding them,
Views are set to alpha = 0 (or INVISIBLE pre-11) only after they
have been determined to be postponed.
Test: gradlew connectedCheck in fragments/
Change-Id: I474af8ca19f7544afef28067e8d11749ef3571b4
diff --git a/fragment/java/android/support/v4/app/Fragment.java b/fragment/java/android/support/v4/app/Fragment.java
index 0c7300e..881c2b4 100644
--- a/fragment/java/android/support/v4/app/Fragment.java
+++ b/fragment/java/android/support/v4/app/Fragment.java
@@ -321,6 +321,11 @@
// True if mHidden has been changed and the animation should be scheduled.
boolean mHiddenChanged;
+ // The alpha of the view when the view was added and then postponed. If the value is less
+ // than zero, this means that the view's add was canceled and should not participate in
+ // removal animations.
+ float mPostponedAlpha;
+
/**
* State information that has been retrieved from a fragment instance
* through {@link FragmentManager#saveFragmentInstanceState(Fragment)
diff --git a/fragment/java/android/support/v4/app/FragmentManager.java b/fragment/java/android/support/v4/app/FragmentManager.java
index e7d13ff..587bda1 100644
--- a/fragment/java/android/support/v4/app/FragmentManager.java
+++ b/fragment/java/android/support/v4/app/FragmentManager.java
@@ -32,6 +32,7 @@
import android.support.annotation.RestrictTo;
import android.support.annotation.StringRes;
import android.support.v4.os.BuildCompat;
+import android.support.v4.util.ArraySet;
import android.support.v4.util.DebugUtils;
import android.support.v4.util.LogWriter;
import android.support.v4.util.Pair;
@@ -1382,10 +1383,12 @@
if (f.mView != null && f.mContainer != null) {
Animation anim = null;
if (mCurState > Fragment.INITIALIZING && !mDestroyed
- && f.mView.getVisibility() == View.VISIBLE) {
+ && f.mView.getVisibility() == View.VISIBLE
+ && f.mPostponedAlpha >= 0) {
anim = loadAnimation(f, transit, false,
transitionStyle);
}
+ f.mPostponedAlpha = 0;
if (anim != null) {
final Fragment fragment = f;
f.setAnimatingAway(f.mView);
@@ -1540,7 +1543,12 @@
}
if (f.mIsNewlyAdded && f.mContainer != null) {
// Make it visible and run the animations
- f.mView.setVisibility(View.VISIBLE);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ f.mView.setVisibility(View.VISIBLE);
+ } else if (f.mPostponedAlpha > 0f) {
+ f.mView.setAlpha(f.mPostponedAlpha);
+ }
+ f.mPostponedAlpha = 0f;
f.mIsNewlyAdded = false;
// run animations:
Animation anim = loadAnimation(f, f.getNextTransition(), true,
@@ -2135,9 +2143,11 @@
int postponeIndex = endIndex;
if (allowOptimization) {
- moveFragmentsToInvisible();
+ ArraySet<Fragment> addedFragments = new ArraySet<>();
+ addAddedFragments(addedFragments);
postponeIndex = postponePostponableTransactions(records, isRecordPop,
- startIndex, endIndex);
+ startIndex, endIndex, addedFragments);
+ makeRemovedFragmentsInvisible(addedFragments);
}
if (postponeIndex != startIndex && allowOptimization) {
@@ -2161,6 +2171,30 @@
}
/**
+ * Any fragments that were removed because they have been postponed should have their views
+ * made invisible by setting their alpha to 0 on API >= 11 or setting visibility to INVISIBLE
+ * on API < 11.
+ *
+ * @param fragments The fragments that were added during operation execution. Only the ones
+ * that are no longer added will have their alpha changed.
+ */
+ private void makeRemovedFragmentsInvisible(ArraySet<Fragment> fragments) {
+ final int numAdded = fragments.size();
+ for (int i = 0; i < numAdded; i++) {
+ final Fragment fragment = fragments.valueAt(i);
+ if (!fragment.mAdded) {
+ final View view = fragment.getView();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ fragment.getView().setVisibility(View.INVISIBLE);
+ } else {
+ fragment.mPostponedAlpha = view.getAlpha();
+ view.setAlpha(0f);
+ }
+ }
+ }
+ }
+
+ /**
* Examine all transactions and determine which ones are marked as postponed. Those will
* have their operations rolled back and moved to the end of the record list (up to endIndex).
* It will also add the postponed transaction to the queue.
@@ -2173,7 +2207,8 @@
* postponed.
*/
private int postponePostponableTransactions(ArrayList<BackStackRecord> records,
- ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {
+ ArrayList<Boolean> isRecordPop, int startIndex, int endIndex,
+ ArraySet<Fragment> added) {
int postponeIndex = endIndex;
for (int i = endIndex - 1; i >= startIndex; i--) {
final BackStackRecord record = records.get(i);
@@ -2204,7 +2239,7 @@
}
// different views may be visible now
- moveFragmentsToInvisible();
+ addAddedFragments(added);
}
}
return postponeIndex;
@@ -2237,15 +2272,26 @@
}
if (moveToState) {
moveToState(mCurState, true);
- } else if (mActive != null) {
+ }
+
+ if (mActive != null) {
final int numActive = mActive.size();
for (int i = 0; i < numActive; i++) {
// Allow added fragments to be removed during the pop since we aren't going
// to move them to the final state with moveToState(mCurState).
Fragment fragment = mActive.get(i);
- if (fragment.mView != null && fragment.mIsNewlyAdded
+ if (fragment != null && fragment.mView != null && fragment.mIsNewlyAdded
&& record.interactsWith(fragment.mContainerId)) {
- fragment.mIsNewlyAdded = false;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
+ && fragment.mPostponedAlpha > 0) {
+ fragment.mView.setAlpha(fragment.mPostponedAlpha);
+ }
+ if (moveToState) {
+ fragment.mPostponedAlpha = 0;
+ } else {
+ fragment.mPostponedAlpha = -1;
+ fragment.mIsNewlyAdded = false;
+ }
}
}
}
@@ -2309,10 +2355,11 @@
/**
* Ensure that fragments that are added are moved to at least the CREATED state.
- * Any newly-added Views are made INVISIBLE so that the Transaction can be postponed
- * with {@link Fragment#postponeEnterTransition()}.
+ * Any newly-added Views are inserted into {@code added} so that the Transaction can be
+ * postponed with {@link Fragment#postponeEnterTransition()}. They will later be made
+ * invisible (by setting their alpha to 0) if they have been removed when postponed.
*/
- private void moveFragmentsToInvisible() {
+ private void addAddedFragments(ArraySet<Fragment> added) {
if (mCurState < Fragment.CREATED) {
return;
}
@@ -2325,7 +2372,7 @@
moveToState(fragment, state, fragment.getNextAnim(), fragment.getNextTransition(),
false);
if (fragment.mView != null && !fragment.mHidden && fragment.mIsNewlyAdded) {
- fragment.mView.setVisibility(View.INVISIBLE);
+ added.add(fragment);
}
}
}
diff --git a/fragment/java/android/support/v4/app/FragmentTransition.java b/fragment/java/android/support/v4/app/FragmentTransition.java
index aa64859..71ac889 100644
--- a/fragment/java/android/support/v4/app/FragmentTransition.java
+++ b/fragment/java/android/support/v4/app/FragmentTransition.java
@@ -1042,7 +1042,8 @@
case BackStackRecord.OP_DETACH:
if (isOptimizedTransaction) {
setFirstOut = !fragment.mAdded && fragment.mView != null
- && fragment.mView.getVisibility() == View.VISIBLE;
+ && fragment.mView.getVisibility() == View.VISIBLE
+ && fragment.mPostponedAlpha >= 0;
} else {
setFirstOut = fragment.mAdded && !fragment.mHidden;
}
diff --git a/fragment/tests/java/android/support/v4/app/FragmentAnimationTest.java b/fragment/tests/java/android/support/v4/app/FragmentAnimationTest.java
index 873e36d..33e20d0 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentAnimationTest.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentAnimationTest.java
@@ -22,6 +22,7 @@
import static org.junit.Assert.assertTrue;
import android.app.Instrumentation;
+import android.os.Build;
import android.os.Parcelable;
import android.support.annotation.AnimRes;
import android.support.fragment.test.R;
@@ -295,6 +296,9 @@
assertPostponed(fragment2, 0);
assertNotNull(fragment1.getView());
assertEquals(View.VISIBLE, fragment1.getView().getVisibility());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ assertEquals(1f, fragment1.getView().getAlpha(), 0f);
+ }
assertTrue(ViewCompat.isAttachedToWindow(fragment1.getView()));
fragment2.startPostponedEnterTransition();
@@ -335,6 +339,9 @@
assertNotNull(fragment1.getView());
assertEquals(View.VISIBLE, fragment1.getView().getVisibility());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ assertEquals(1f, fragment1.getView().getAlpha(), 0f);
+ }
assertTrue(ViewCompat.isAttachedToWindow(fragment1.getView()));
assertTrue(fragment1.isAdded());
@@ -456,7 +463,12 @@
private void assertPostponed(AnimatorFragment fragment, int expectedAnimators)
throws InterruptedException {
assertTrue(fragment.mOnCreateViewCalled);
- assertEquals(View.INVISIBLE, fragment.getView().getVisibility());
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ assertEquals(View.INVISIBLE, fragment.getView().getVisibility());
+ } else {
+ assertEquals(View.VISIBLE, fragment.getView().getVisibility());
+ assertEquals(0f, fragment.getView().getAlpha(), 0f);
+ }
assertEquals(expectedAnimators, fragment.numAnimators);
}
diff --git a/fragment/tests/java/android/support/v4/app/FragmentOptimizationTest.java b/fragment/tests/java/android/support/v4/app/FragmentOptimizationTest.java
index 08be463..06abd69 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentOptimizationTest.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentOptimizationTest.java
@@ -24,7 +24,9 @@
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.v4.app.test.FragmentTestActivity;
+import android.view.View;
import android.view.ViewGroup;
+import android.widget.EditText;
import org.junit.Before;
import org.junit.Rule;
@@ -644,4 +646,42 @@
assertEquals(1, fragment1.onCreateViewCount);
}
+ // Test that a fragment view that is created with focus has focus after the transaction
+ // completes.
+ @Test
+ public void focusedView() throws Throwable {
+ FragmentTestUtil.setContentView(mActivityRule, R.layout.double_container);
+ mContainer = (ViewGroup) mActivityRule.getActivity().findViewById(R.id.fragmentContainer1);
+ final EditText firstEditText = new EditText(mContainer.getContext());
+ mInstrumentation.runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mContainer.addView(firstEditText);
+ firstEditText.requestFocus();
+ }
+ });
+ assertTrue(firstEditText.isFocused());
+ final CountCallsFragment fragment1 = new CountCallsFragment();
+ final CountCallsFragment fragment2 = new CountCallsFragment();
+ fragment2.setLayoutId(R.layout.with_edit_text);
+ mInstrumentation.runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mFM.beginTransaction()
+ .add(R.id.fragmentContainer2, fragment1)
+ .addToBackStack(null)
+ .setAllowOptimization(true)
+ .commit();
+ mFM.beginTransaction()
+ .replace(R.id.fragmentContainer2, fragment2)
+ .addToBackStack(null)
+ .setAllowOptimization(true)
+ .commit();
+ mFM.executePendingTransactions();
+ }
+ });
+ final View editText = fragment2.getView().findViewById(R.id.editText);
+ assertTrue(editText.isFocused());
+ assertFalse(firstEditText.isFocused());
+ }
}
diff --git a/fragment/tests/java/android/support/v4/app/PostponedTransitionTest.java b/fragment/tests/java/android/support/v4/app/PostponedTransitionTest.java
index fe41976..0f07b89 100644
--- a/fragment/tests/java/android/support/v4/app/PostponedTransitionTest.java
+++ b/fragment/tests/java/android/support/v4/app/PostponedTransitionTest.java
@@ -664,6 +664,9 @@
assertNull(fragment.getView());
assertNotNull(mBeginningFragment.getView());
assertEquals(View.VISIBLE, mBeginningFragment.getView().getVisibility());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ assertEquals(1f, mBeginningFragment.getView().getAlpha(), 0f);
+ }
assertTrue(mBeginningFragment.getView().isAttachedToWindow());
}
@@ -695,6 +698,9 @@
assertNotNull(fragment2);
assertNotNull(fragment2.getView());
assertEquals(View.VISIBLE, fragment2.getView().getVisibility());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ assertEquals(1f, fragment2.getView().getAlpha(), 0f);
+ }
assertTrue(fragment2.isResumed());
assertTrue(fragment2.isAdded());
assertTrue(fragment2.getView().isAttachedToWindow());
@@ -726,7 +732,12 @@
assertTrue(fromFragment.getView().isAttachedToWindow());
assertTrue(toFragment.getView().isAttachedToWindow());
assertEquals(View.VISIBLE, fromFragment.getView().getVisibility());
- assertEquals(View.INVISIBLE, toFragment.getView().getVisibility());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ assertEquals(View.VISIBLE, toFragment.getView().getVisibility());
+ assertEquals(0f, toFragment.getView().getAlpha(), 0f);
+ } else {
+ assertEquals(View.INVISIBLE, toFragment.getView().getVisibility());
+ }
assureNoTransition(fromFragment);
assureNoTransition(toFragment);
assertTrue(fromFragment.isResumed());
diff --git a/fragment/tests/res/layout/with_edit_text.xml b/fragment/tests/res/layout/with_edit_text.xml
new file mode 100644
index 0000000..7fd21a0
--- /dev/null
+++ b/fragment/tests/res/layout/with_edit_text.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <EditText android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/editText"
+ android:text="@string/hello">
+ <requestFocus/>
+ </EditText>
+</LinearLayout>