blob: 385fabd2ec846fd2b3cd2d15707b9f5abe73b600 [file] [log] [blame]
/*
* Copyright 2018 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.
*/
package androidx.fragment.app;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.app.Instrumentation;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.TranslateAnimation;
import androidx.annotation.AnimRes;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.test.FragmentTestActivity;
import androidx.fragment.test.R;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@MediumTest
@RunWith(AndroidJUnit4.class)
public class FragmentAnimationTest {
// These are pretend resource IDs for animators. We don't need real ones since we
// load them by overriding onCreateAnimator
@AnimRes
private static final int ENTER = 1;
@AnimRes
private static final int EXIT = 2;
@AnimRes
private static final int POP_ENTER = 3;
@AnimRes
private static final int POP_EXIT = 4;
@Rule
public ActivityTestRule<FragmentTestActivity> mActivityRule =
new ActivityTestRule<FragmentTestActivity>(FragmentTestActivity.class);
private Instrumentation mInstrumentation;
@Before
public void setupContainer() {
mInstrumentation = InstrumentationRegistry.getInstrumentation();
FragmentTestUtil.setContentView(mActivityRule, R.layout.simple_container);
}
// Ensure that adding and popping a Fragment uses the enter and popExit animators
@Test
public void addAnimators() throws Throwable {
final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
// One fragment with a view
final AnimatorFragment fragment = new AnimatorFragment();
fm.beginTransaction()
.setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
.add(R.id.fragmentContainer, fragment)
.addToBackStack(null)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
assertEnterPopExit(fragment);
}
// Ensure that removing and popping a Fragment uses the exit and popEnter animators
@Test
public void removeAnimators() throws Throwable {
final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
// One fragment with a view
final AnimatorFragment fragment = new AnimatorFragment();
fm.beginTransaction().add(R.id.fragmentContainer, fragment, "1").commit();
FragmentTestUtil.waitForExecution(mActivityRule);
fm.beginTransaction()
.setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
.remove(fragment)
.addToBackStack(null)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
assertExitPopEnter(fragment);
}
// Ensure that showing and popping a Fragment uses the enter and popExit animators
@Test
public void showAnimators() throws Throwable {
final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
// One fragment with a view
final AnimatorFragment fragment = new AnimatorFragment();
fm.beginTransaction().add(R.id.fragmentContainer, fragment).hide(fragment).commit();
FragmentTestUtil.waitForExecution(mActivityRule);
fm.beginTransaction()
.setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
.show(fragment)
.addToBackStack(null)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
assertEnterPopExit(fragment);
}
// Ensure that hiding and popping a Fragment uses the exit and popEnter animators
@Test
public void hideAnimators() throws Throwable {
final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
// One fragment with a view
final AnimatorFragment fragment = new AnimatorFragment();
fm.beginTransaction().add(R.id.fragmentContainer, fragment, "1").commit();
FragmentTestUtil.waitForExecution(mActivityRule);
fm.beginTransaction()
.setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
.hide(fragment)
.addToBackStack(null)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
assertExitPopEnter(fragment);
}
// Ensure that attaching and popping a Fragment uses the enter and popExit animators
@Test
public void attachAnimators() throws Throwable {
final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
// One fragment with a view
final AnimatorFragment fragment = new AnimatorFragment();
fm.beginTransaction().add(R.id.fragmentContainer, fragment).detach(fragment).commit();
FragmentTestUtil.waitForExecution(mActivityRule);
fm.beginTransaction()
.setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
.attach(fragment)
.addToBackStack(null)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
assertEnterPopExit(fragment);
}
// Ensure that detaching and popping a Fragment uses the exit and popEnter animators
@Test
public void detachAnimators() throws Throwable {
final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
// One fragment with a view
final AnimatorFragment fragment = new AnimatorFragment();
fm.beginTransaction().add(R.id.fragmentContainer, fragment, "1").commit();
FragmentTestUtil.waitForExecution(mActivityRule);
fm.beginTransaction()
.setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
.detach(fragment)
.addToBackStack(null)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
assertExitPopEnter(fragment);
}
// Replace should exit the existing fragments and enter the added fragment, then
// popping should popExit the removed fragment and popEnter the added fragments
@Test
public void replaceAnimators() throws Throwable {
final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
// One fragment with a view
final AnimatorFragment fragment1 = new AnimatorFragment();
final AnimatorFragment fragment2 = new AnimatorFragment();
fm.beginTransaction()
.add(R.id.fragmentContainer, fragment1, "1")
.add(R.id.fragmentContainer, fragment2, "2")
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
final AnimatorFragment fragment3 = new AnimatorFragment();
fm.beginTransaction()
.setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
.replace(R.id.fragmentContainer, fragment3)
.addToBackStack(null)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
assertFragmentAnimation(fragment1, 1, false, EXIT);
assertFragmentAnimation(fragment2, 1, false, EXIT);
assertFragmentAnimation(fragment3, 1, true, ENTER);
fm.popBackStack();
FragmentTestUtil.waitForExecution(mActivityRule);
assertFragmentAnimation(fragment3, 2, false, POP_EXIT);
final AnimatorFragment replacement1 = (AnimatorFragment) fm.findFragmentByTag("1");
final AnimatorFragment replacement2 = (AnimatorFragment) fm.findFragmentByTag("1");
int expectedAnimations = replacement1 == fragment1 ? 2 : 1;
assertFragmentAnimation(replacement1, expectedAnimations, true, POP_ENTER);
assertFragmentAnimation(replacement2, expectedAnimations, true, POP_ENTER);
}
// Ensure that adding and popping a Fragment uses the enter and popExit animators,
// but the animators are delayed when an entering Fragment is postponed.
@Test
public void postponedAddAnimators() throws Throwable {
final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
final AnimatorFragment fragment = new AnimatorFragment();
fragment.postponeEnterTransition();
fm.beginTransaction()
.setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
.add(R.id.fragmentContainer, fragment)
.addToBackStack(null)
.setReorderingAllowed(true)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
assertPostponed(fragment, 0);
fragment.startPostponedEnterTransition();
FragmentTestUtil.waitForExecution(mActivityRule);
assertEnterPopExit(fragment);
}
// Ensure that removing and popping a Fragment uses the exit and popEnter animators,
// but the animators are delayed when an entering Fragment is postponed.
@Test
public void postponedRemoveAnimators() throws Throwable {
final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
final AnimatorFragment fragment = new AnimatorFragment();
fm.beginTransaction().add(R.id.fragmentContainer, fragment, "1").commit();
FragmentTestUtil.waitForExecution(mActivityRule);
fm.beginTransaction()
.setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
.remove(fragment)
.addToBackStack(null)
.setReorderingAllowed(true)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
assertExitPostponedPopEnter(fragment);
}
// Ensure that adding and popping a Fragment is postponed in both directions
// when the fragments have been marked for postponing.
@Test
public void postponedAddRemove() throws Throwable {
final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
final AnimatorFragment fragment1 = new AnimatorFragment();
fm.beginTransaction()
.add(R.id.fragmentContainer, fragment1)
.addToBackStack(null)
.setReorderingAllowed(true)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
final AnimatorFragment fragment2 = new AnimatorFragment();
fragment2.postponeEnterTransition();
fm.beginTransaction()
.setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
.replace(R.id.fragmentContainer, fragment2)
.addToBackStack(null)
.setReorderingAllowed(true)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
assertPostponed(fragment2, 0);
assertNotNull(fragment1.getView());
assertEquals(View.VISIBLE, fragment1.getView().getVisibility());
assertEquals(1f, fragment1.getView().getAlpha(), 0f);
assertTrue(ViewCompat.isAttachedToWindow(fragment1.getView()));
fragment2.startPostponedEnterTransition();
FragmentTestUtil.waitForExecution(mActivityRule);
assertExitPostponedPopEnter(fragment1);
}
// Popping a postponed transaction should result in no animators
@Test
public void popPostponed() throws Throwable {
final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
final AnimatorFragment fragment1 = new AnimatorFragment();
fm.beginTransaction()
.add(R.id.fragmentContainer, fragment1)
.setReorderingAllowed(true)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
assertEquals(0, fragment1.numAnimators);
final AnimatorFragment fragment2 = new AnimatorFragment();
fragment2.postponeEnterTransition();
fm.beginTransaction()
.setCustomAnimations(ENTER, EXIT, POP_ENTER, POP_EXIT)
.replace(R.id.fragmentContainer, fragment2)
.addToBackStack(null)
.setReorderingAllowed(true)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
assertPostponed(fragment2, 0);
// Now pop the postponed transaction
FragmentTestUtil.popBackStackImmediate(mActivityRule);
assertNotNull(fragment1.getView());
assertEquals(View.VISIBLE, fragment1.getView().getVisibility());
assertEquals(1f, fragment1.getView().getAlpha(), 0f);
assertTrue(ViewCompat.isAttachedToWindow(fragment1.getView()));
assertTrue(fragment1.isAdded());
assertNull(fragment2.getView());
assertFalse(fragment2.isAdded());
assertEquals(0, fragment1.numAnimators);
assertEquals(0, fragment2.numAnimators);
assertNull(fragment1.animation);
assertNull(fragment2.animation);
}
// Make sure that if the state was saved while a Fragment was animating that its
// state is proper after restoring.
@Test
public void saveWhileAnimatingAway() throws Throwable {
final FragmentController fc1 = FragmentTestUtil.createController(mActivityRule);
FragmentTestUtil.resume(mActivityRule, fc1, null);
final FragmentManager fm1 = fc1.getSupportFragmentManager();
StrictViewFragment fragment1 = new StrictViewFragment();
fragment1.setLayoutId(R.layout.scene1);
fm1.beginTransaction()
.add(R.id.fragmentContainer, fragment1, "1")
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
StrictViewFragment fragment2 = new StrictViewFragment();
fm1.beginTransaction()
.setCustomAnimations(0, 0, 0, R.anim.long_fade_out)
.replace(R.id.fragmentContainer, fragment2, "2")
.addToBackStack(null)
.commit();
mInstrumentation.runOnMainSync(new Runnable() {
@Override
public void run() {
fm1.executePendingTransactions();
}
});
FragmentTestUtil.waitForExecution(mActivityRule);
fm1.popBackStack();
mInstrumentation.runOnMainSync(new Runnable() {
@Override
public void run() {
fm1.executePendingTransactions();
}
});
FragmentTestUtil.waitForExecution(mActivityRule);
// Now fragment2 should be animating away
assertFalse(fragment2.isAdded());
assertEquals(fragment2, fm1.findFragmentByTag("2")); // still exists because it is animating
Pair<Parcelable, FragmentManagerNonConfig> state =
FragmentTestUtil.destroy(mActivityRule, fc1);
final FragmentController fc2 = FragmentTestUtil.createController(mActivityRule);
FragmentTestUtil.resume(mActivityRule, fc2, state);
final FragmentManager fm2 = fc2.getSupportFragmentManager();
Fragment fragment2restored = fm2.findFragmentByTag("2");
assertNull(fragment2restored);
Fragment fragment1restored = fm2.findFragmentByTag("1");
assertNotNull(fragment1restored);
assertNotNull(fragment1restored.getView());
}
// When an animation is running on a Fragment's View, the view shouldn't be
// prevented from being removed. There's no way to directly test this, so we have to
// test to see if the animation is still running.
@Test
public void clearAnimations() throws Throwable {
final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
final StrictViewFragment fragment1 = new StrictViewFragment();
fm.beginTransaction()
.add(R.id.fragmentContainer, fragment1)
.setReorderingAllowed(true)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
final View fragmentView = fragment1.getView();
final TranslateAnimation xAnimation = new TranslateAnimation(0, 1000, 0, 0);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
fragmentView.startAnimation(xAnimation);
}
});
FragmentTestUtil.waitForExecution(mActivityRule);
FragmentTestUtil.popBackStackImmediate(mActivityRule);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
assertNull(fragmentView.getAnimation());
}
});
}
// When a view is animated out, is parent should be null after the animation completes
@Test
public void parentNullAfterAnimation() throws Throwable {
final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
final EndAnimationListenerFragment fragment1 = new EndAnimationListenerFragment();
fm.beginTransaction()
.add(R.id.fragmentContainer, fragment1)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
final EndAnimationListenerFragment fragment2 = new EndAnimationListenerFragment();
fm.beginTransaction()
.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out,
android.R.anim.fade_in, android.R.anim.fade_out)
.replace(R.id.fragmentContainer, fragment2)
.addToBackStack(null)
.commit();
FragmentTestUtil.waitForExecution(mActivityRule);
assertTrue(fragment1.exitLatch.await(1, TimeUnit.SECONDS));
assertTrue(fragment2.enterLatch.await(1, TimeUnit.SECONDS));
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
assertNotNull(fragment1.view);
assertNotNull(fragment2.view);
assertNull(fragment1.view.getParent());
}
});
// Now pop the transaction
FragmentTestUtil.popBackStackImmediate(mActivityRule);
assertTrue(fragment2.exitLatch.await(1, TimeUnit.SECONDS));
assertTrue(fragment1.enterLatch.await(1, TimeUnit.SECONDS));
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
assertNull(fragment2.view.getParent());
}
});
}
private void assertEnterPopExit(AnimatorFragment fragment) throws Throwable {
assertFragmentAnimation(fragment, 1, true, ENTER);
final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
fm.popBackStack();
FragmentTestUtil.waitForExecution(mActivityRule);
assertFragmentAnimation(fragment, 2, false, POP_EXIT);
}
private void assertExitPopEnter(AnimatorFragment fragment) throws Throwable {
assertFragmentAnimation(fragment, 1, false, EXIT);
final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
fm.popBackStack();
FragmentTestUtil.waitForExecution(mActivityRule);
AnimatorFragment replacement = (AnimatorFragment) fm.findFragmentByTag("1");
boolean isSameFragment = replacement == fragment;
int expectedAnimators = isSameFragment ? 2 : 1;
assertFragmentAnimation(replacement, expectedAnimators, true, POP_ENTER);
}
private void assertExitPostponedPopEnter(AnimatorFragment fragment) throws Throwable {
assertFragmentAnimation(fragment, 1, false, EXIT);
fragment.postponeEnterTransition();
FragmentTestUtil.popBackStackImmediate(mActivityRule);
assertPostponed(fragment, 1);
fragment.startPostponedEnterTransition();
FragmentTestUtil.waitForExecution(mActivityRule);
assertFragmentAnimation(fragment, 2, true, POP_ENTER);
}
private void assertFragmentAnimation(AnimatorFragment fragment, int numAnimators,
boolean isEnter, int animatorResourceId) throws InterruptedException {
assertEquals(numAnimators, fragment.numAnimators);
assertEquals(isEnter, fragment.enter);
assertEquals(animatorResourceId, fragment.resourceId);
assertNotNull(fragment.animation);
assertTrue(FragmentTestUtil.waitForAnimationEnd(1000, fragment.animation));
assertTrue(fragment.animation.hasStarted());
}
private void assertPostponed(AnimatorFragment fragment, int expectedAnimators)
throws InterruptedException {
assertTrue(fragment.mOnCreateViewCalled);
assertEquals(View.VISIBLE, fragment.getView().getVisibility());
assertEquals(0f, fragment.getView().getAlpha(), 0f);
assertEquals(expectedAnimators, fragment.numAnimators);
}
public static class AnimatorFragment extends StrictViewFragment {
public int numAnimators;
public Animation animation;
public boolean enter;
public int resourceId;
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
if (nextAnim == 0) {
return null;
}
this.numAnimators++;
this.animation = new TranslateAnimation(-10, 0, 0, 0);
this.animation.setDuration(1);
this.resourceId = nextAnim;
this.enter = enter;
return this.animation;
}
}
public static class EndAnimationListenerFragment extends StrictViewFragment {
public View view;
public final CountDownLatch enterLatch = new CountDownLatch(1);
public final CountDownLatch exitLatch = new CountDownLatch(1);
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if (view != null) {
return view;
}
view = super.onCreateView(inflater, container, savedInstanceState);
return view;
}
@Override
public Animation onCreateAnimation(int transit, final boolean enter, int nextAnim) {
if (nextAnim == 0) {
return null;
}
Animation anim = AnimationUtils.loadAnimation(getActivity(), nextAnim);
if (anim != null) {
anim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (enter) {
enterLatch.countDown();
} else {
exitLatch.countDown();
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
}
return anim;
}
}
}