| /* |
| * 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. |
| */ |
| package android.transition.cts; |
| |
| import static com.android.compatibility.common.util.CtsMockitoUtils.within; |
| |
| import static junit.framework.Assert.assertFalse; |
| import static junit.framework.Assert.assertTrue; |
| import static junit.framework.Assert.fail; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.mockito.Matchers.any; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.times; |
| import static org.mockito.Mockito.verify; |
| |
| import android.app.ActivityOptions; |
| import android.app.SharedElementCallback; |
| import android.content.Intent; |
| import android.graphics.Bitmap; |
| import android.graphics.drawable.BitmapDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.os.Bundle; |
| import android.transition.Fade; |
| import android.transition.Transition; |
| import android.transition.Transition.TransitionListener; |
| import android.transition.TransitionListenerAdapter; |
| import android.view.View; |
| import android.view.ViewGroup; |
| |
| import androidx.test.filters.MediumTest; |
| import androidx.test.runner.AndroidJUnit4; |
| |
| import com.android.compatibility.common.util.PollingCheck; |
| import com.android.compatibility.common.util.transition.TargetTracking; |
| import com.android.compatibility.common.util.transition.TrackingTransition; |
| import com.android.compatibility.common.util.transition.TrackingVisibility; |
| |
| import org.junit.After; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.stream.Collectors; |
| |
| @MediumTest |
| @RunWith(AndroidJUnit4.class) |
| public class ActivityTransitionTest extends BaseTransitionTest { |
| private TransitionListener mExitListener; |
| private TransitionListener mReenterListener; |
| private TransitionListener mSharedElementReenterListener; |
| private TrackingVisibility mExitTransition; |
| private TrackingVisibility mReenterTransition; |
| private TrackingTransition mSharedElementReenterTransition; |
| |
| @Override |
| public void setup() { |
| super.setup(); |
| setTransitions(new TrackingVisibility(), new TrackingVisibility(), |
| new TrackingTransition()); |
| } |
| |
| private void setTransitions(TrackingVisibility exit, TrackingVisibility reenter, |
| TrackingTransition sharedElementReenter) { |
| mExitTransition = exit; |
| mExitListener = mock(TransitionListener.class); |
| mExitTransition.addListener(mExitListener); |
| mActivity.getWindow().setExitTransition(mExitTransition); |
| |
| mReenterTransition = reenter; |
| mReenterListener = mock(TransitionListener.class); |
| mReenterTransition.addListener(mReenterListener); |
| mActivity.getWindow().setReenterTransition(mReenterTransition); |
| |
| mSharedElementReenterTransition = sharedElementReenter; |
| mSharedElementReenterListener = mock(TransitionListener.class); |
| mSharedElementReenterTransition.addListener(mSharedElementReenterListener); |
| mActivity.getWindow().setSharedElementReenterTransition(mSharedElementReenterTransition); |
| } |
| |
| @After |
| public void cleanup() throws Throwable { |
| if (TargetActivity.sLastCreated != null) { |
| mActivityRule.runOnUiThread(() -> TargetActivity.sLastCreated.finish()); |
| } |
| TargetActivity.sLastCreated = null; |
| } |
| |
| // When using ActivityOptions.makeBasic(), no transitions should run |
| @Test |
| public void testMakeBasic() throws Throwable { |
| assertFalse(mActivity.isActivityTransitionRunning()); |
| mActivityRule.runOnUiThread(() -> { |
| Intent intent = new Intent(mActivity, TargetActivity.class); |
| ActivityOptions activityOptions = |
| ActivityOptions.makeBasic(); |
| mActivity.startActivity(intent, activityOptions.toBundle()); |
| }); |
| |
| assertFalse(mActivity.isActivityTransitionRunning()); |
| |
| TargetActivity targetActivity = waitForTargetActivity(); |
| assertFalse(targetActivity.isActivityTransitionRunning()); |
| mActivityRule.runOnUiThread(() -> { |
| targetActivity.finish(); |
| }); |
| |
| assertFalse(targetActivity.isActivityTransitionRunning()); |
| assertFalse(mActivity.isActivityTransitionRunning()); |
| } |
| |
| // Views that are outside the visible area only during the shared element start |
| // should not be stripped from the transition. |
| @Test |
| public void viewsNotStripped() throws Throwable { |
| enterScene(R.layout.scene10); |
| mActivityRule.runOnUiThread(() -> { |
| View sharedElement = mActivity.findViewById(R.id.blueSquare); |
| Bundle options = ActivityOptions.makeSceneTransitionAnimation(mActivity, |
| sharedElement, "holder").toBundle(); |
| Intent intent = new Intent(mActivity, TargetActivity.class); |
| intent.putExtra(TargetActivity.EXTRA_LAYOUT_ID, R.layout.scene12); |
| mActivity.startActivity(intent, options); |
| }); |
| |
| TargetActivity targetActivity = waitForTargetActivity(); |
| verify(targetActivity.enterListener, within(3000)).onTransitionEnd(any()); |
| verify(mExitListener, times(1)).onTransitionEnd(any()); |
| |
| // Now check the targets... they should all be there |
| assertTargetContains(targetActivity.enterTransition, |
| R.id.redSquare, R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); |
| assertTargetExcludes(targetActivity.enterTransition, R.id.holder); |
| |
| assertTargetContains(targetActivity.sharedElementEnterTransition, R.id.holder); |
| assertTargetExcludes(targetActivity.sharedElementEnterTransition, |
| R.id.redSquare, R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); |
| |
| assertTargetContains(mExitTransition, R.id.redSquare, R.id.greenSquare, R.id.yellowSquare); |
| assertTargetExcludes(mExitTransition, R.id.blueSquare, R.id.holder); |
| |
| assertEquals(View.VISIBLE, targetActivity.findViewById(R.id.redSquare).getVisibility()); |
| assertEquals(View.VISIBLE, targetActivity.findViewById(R.id.greenSquare).getVisibility()); |
| assertEquals(View.VISIBLE, targetActivity.findViewById(R.id.holder).getVisibility()); |
| |
| assertEquals(1, targetActivity.findViewById(R.id.redSquare).getAlpha(), 0.01f); |
| assertEquals(1, targetActivity.findViewById(R.id.greenSquare).getAlpha(), 0.01f); |
| assertEquals(1, targetActivity.findViewById(R.id.holder).getAlpha(), 0.01f); |
| |
| mActivityRule.runOnUiThread(() -> targetActivity.finishAfterTransition()); |
| verify(mReenterListener, within(3000)).onTransitionEnd(any()); |
| verify(mSharedElementReenterListener, within(3000)).onTransitionEnd(any()); |
| verify(targetActivity.returnListener, times(1)).onTransitionEnd(any()); |
| |
| // return targets are stripped also |
| assertTargetContains(targetActivity.returnTransition, |
| R.id.redSquare, R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); |
| assertTargetExcludes(targetActivity.returnTransition, R.id.holder); |
| |
| assertTargetContains(mReenterTransition, |
| R.id.redSquare, R.id.greenSquare, R.id.yellowSquare); |
| assertTargetExcludes(mReenterTransition, R.id.blueSquare, R.id.holder); |
| |
| assertTargetContains(targetActivity.sharedElementReturnTransition, |
| R.id.holder); |
| assertTargetExcludes(targetActivity.sharedElementReturnTransition, |
| R.id.redSquare, R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); |
| |
| assertTargetContains(mSharedElementReenterTransition, R.id.blueSquare); |
| assertTargetExcludes(mSharedElementReenterTransition, |
| R.id.redSquare, R.id.greenSquare, R.id.yellowSquare); |
| |
| assertEquals(View.VISIBLE, mActivity.findViewById(R.id.redSquare).getVisibility()); |
| assertEquals(View.VISIBLE, mActivity.findViewById(R.id.greenSquare).getVisibility()); |
| assertEquals(View.VISIBLE, mActivity.findViewById(R.id.holder).getVisibility()); |
| |
| assertEquals(1, mActivity.findViewById(R.id.redSquare).getAlpha(), 0.01f); |
| assertEquals(1, mActivity.findViewById(R.id.greenSquare).getAlpha(), 0.01f); |
| assertEquals(1, mActivity.findViewById(R.id.holder).getAlpha(), 0.01f); |
| |
| TargetActivity.sLastCreated = null; |
| } |
| |
| // Views that are outside the visible area during initial layout should be stripped from |
| // the transition. |
| @Test |
| public void viewsStripped() throws Throwable { |
| enterScene(R.layout.scene13); |
| mActivityRule.runOnUiThread(() -> { |
| View sharedElement = mActivity.findViewById(R.id.redSquare); |
| Bundle options = ActivityOptions.makeSceneTransitionAnimation(mActivity, |
| sharedElement, "redSquare").toBundle(); |
| Intent intent = new Intent(mActivity, TargetActivity.class); |
| intent.putExtra(TargetActivity.EXTRA_LAYOUT_ID, R.layout.scene13); |
| mActivity.startActivity(intent, options); |
| }); |
| |
| TargetActivity targetActivity = waitForTargetActivity(); |
| verify(targetActivity.enterListener, within(3000)).onTransitionEnd(any()); |
| verify(mExitListener, times(1)).onTransitionEnd(any()); |
| |
| // Now check the targets... they should all be stripped |
| assertTargetExcludes(targetActivity.enterTransition, R.id.holder, |
| R.id.redSquare, R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); |
| |
| assertTargetExcludes(mExitTransition, R.id.holder, |
| R.id.redSquare, R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); |
| |
| assertTargetContains(targetActivity.sharedElementEnterTransition, R.id.redSquare); |
| assertTargetExcludes(targetActivity.sharedElementEnterTransition, |
| R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); |
| |
| assertEquals(View.VISIBLE, targetActivity.findViewById(R.id.redSquare).getVisibility()); |
| assertEquals(View.VISIBLE, targetActivity.findViewById(R.id.greenSquare).getVisibility()); |
| assertEquals(View.VISIBLE, targetActivity.findViewById(R.id.holder).getVisibility()); |
| |
| assertEquals(1, targetActivity.findViewById(R.id.redSquare).getAlpha(), 0.01f); |
| assertEquals(1, targetActivity.findViewById(R.id.greenSquare).getAlpha(), 0.01f); |
| assertEquals(1, targetActivity.findViewById(R.id.holder).getAlpha(), 0.01f); |
| |
| mActivityRule.runOnUiThread(() -> targetActivity.finishAfterTransition()); |
| verify(mReenterListener, within(3000)).onTransitionEnd(any()); |
| verify(mSharedElementReenterListener, within(3000)).onTransitionEnd(any()); |
| verify(targetActivity.returnListener, times(1)).onTransitionEnd(any()); |
| |
| // return targets are stripped also |
| assertTargetExcludes(targetActivity.returnTransition, |
| R.id.redSquare, R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); |
| |
| assertTargetExcludes(mReenterTransition, R.id.holder, |
| R.id.redSquare, R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); |
| |
| assertTargetContains(targetActivity.sharedElementReturnTransition, |
| R.id.redSquare); |
| assertTargetExcludes(targetActivity.sharedElementReturnTransition, |
| R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); |
| |
| assertTargetContains(mSharedElementReenterTransition, R.id.redSquare); |
| assertTargetExcludes(mSharedElementReenterTransition, |
| R.id.blueSquare, R.id.greenSquare, R.id.yellowSquare); |
| |
| assertEquals(View.VISIBLE, mActivity.findViewById(R.id.greenSquare).getVisibility()); |
| assertEquals(View.VISIBLE, mActivity.findViewById(R.id.holder).getVisibility()); |
| assertEquals(View.VISIBLE, mActivity.findViewById(R.id.redSquare).getVisibility()); |
| |
| assertEquals(1, mActivity.findViewById(R.id.redSquare).getAlpha(), 0.01f); |
| assertEquals(1, mActivity.findViewById(R.id.greenSquare).getAlpha(), 0.01f); |
| assertEquals(1, mActivity.findViewById(R.id.holder).getAlpha(), 0.01f); |
| |
| TargetActivity.sLastCreated = null; |
| } |
| |
| // When an exit transition takes longer than it takes the activity to cover it (and onStop |
| // is called), the exiting views should become visible. |
| @Test |
| public void earlyExitStop() throws Throwable { |
| enterScene(R.layout.scene1); |
| final View hello = mActivity.findViewById(R.id.hello); |
| final View red = mActivity.findViewById(R.id.redSquare); |
| final View green = mActivity.findViewById(R.id.greenSquare); |
| mActivityRule.runOnUiThread(() -> { |
| Fade fade = new Fade(); |
| fade.setDuration(10000); |
| fade.addListener(mExitListener); |
| mActivity.getWindow().setExitTransition(fade); |
| Bundle options = ActivityOptions.makeSceneTransitionAnimation(mActivity).toBundle(); |
| Intent intent = new Intent(mActivity, TargetActivity.class); |
| intent.putExtra(TargetActivity.EXTRA_LAYOUT_ID, R.layout.scene4); |
| mActivity.startActivity(intent, options); |
| }); |
| |
| TargetActivity targetActivity = waitForTargetActivity(); |
| verify(targetActivity.enterListener, within(3000)).onTransitionEnd(any()); |
| verify(mExitListener, within(3000)).onTransitionEnd(any()); |
| |
| mActivityRule.runOnUiThread(() -> { |
| // Verify that the exited views have an alpha of 1 and are visible |
| assertEquals(1.0f, hello.getAlpha(), 0.01f); |
| assertEquals(1.0f, red.getAlpha(), 0.01f); |
| assertEquals(1.0f, green.getAlpha(), 0.01f); |
| |
| assertEquals(View.VISIBLE, hello.getVisibility()); |
| assertEquals(View.VISIBLE, red.getVisibility()); |
| assertEquals(View.VISIBLE, green.getVisibility()); |
| targetActivity.finish(); |
| }); |
| } |
| |
| @Test |
| public void testAnimationQuery() throws Throwable { |
| enterScene(R.layout.scene1); |
| assertFalse(mActivity.isActivityTransitionRunning()); |
| mActivityRule.runOnUiThread(() -> { |
| mActivity.getWindow().setExitTransition(new Fade()); |
| Intent intent = new Intent(mActivity, TargetActivity.class); |
| ActivityOptions activityOptions = |
| ActivityOptions.makeSceneTransitionAnimation(mActivity); |
| mActivity.startActivity(intent, activityOptions.toBundle()); |
| }); |
| |
| assertTrue(mActivity.isActivityTransitionRunning()); |
| |
| TargetActivity targetActivity = waitForTargetActivity(); |
| assertTrue(targetActivity.isActivityTransitionRunning()); |
| mActivityRule.runOnUiThread(() -> { }); |
| PollingCheck.waitFor(() -> !targetActivity.isActivityTransitionRunning()); |
| |
| assertFalse(mActivity.isActivityTransitionRunning()); |
| mActivityRule.runOnUiThread(() -> { |
| targetActivity.finishAfterTransition(); |
| // The target activity transition should start right away |
| assertTrue(targetActivity.isActivityTransitionRunning()); |
| }); |
| |
| // The source activity transition should start sometime later |
| PollingCheck.waitFor(() -> mActivity.isActivityTransitionRunning()); |
| PollingCheck.waitFor(() -> !mActivity.isActivityTransitionRunning()); |
| } |
| |
| // Views that are excluded from the exit/enter transition shouldn't change visibility |
| @Test |
| public void untargetedViews() throws Throwable { |
| enterScene(R.layout.scene10); |
| |
| final View redSquare = mActivity.findViewById(R.id.redSquare); |
| |
| setTransitions(new TrackingVisibilityWithAnimator(), new TrackingVisibilityWithAnimator(), |
| new TrackingTransition()); |
| TransitionListener redSquareValidator = new TransitionListenerAdapter() { |
| @Override |
| public void onTransitionStart(Transition transition) { |
| assertEquals(View.VISIBLE, redSquare.getVisibility()); |
| } |
| |
| @Override |
| public void onTransitionEnd(Transition transition) { |
| assertEquals(View.VISIBLE, redSquare.getVisibility()); |
| } |
| }; |
| mExitTransition.addListener(redSquareValidator); |
| mReenterTransition.addListener(redSquareValidator); |
| |
| mExitTransition.excludeTarget(R.id.redSquare, true); |
| mReenterTransition.excludeTarget(R.id.redSquare, true); |
| |
| mActivity.runOnUiThread(() -> { |
| Bundle options = ActivityOptions.makeSceneTransitionAnimation(mActivity).toBundle(); |
| Intent intent = new Intent(mActivity, TargetActivity.class); |
| intent.putExtra(TargetActivity.EXTRA_LAYOUT_ID, R.layout.scene12); |
| intent.putExtra(TargetActivity.EXTRA_EXCLUDE_ID, R.id.redSquare); |
| intent.putExtra(TargetActivity.EXTRA_USE_ANIMATOR, true); |
| mActivity.startActivity(intent, options); |
| }); |
| |
| verify(mExitListener, within(3000)).onTransitionEnd(any()); |
| |
| TargetActivity targetActivity = waitForTargetActivity(); |
| |
| assertTrue(targetActivity.transitionComplete.await(1, TimeUnit.SECONDS)); |
| assertEquals(View.VISIBLE, targetActivity.startVisibility); |
| assertEquals(View.VISIBLE, targetActivity.endVisibility); |
| |
| // Reset so that we know that they are modified when returning |
| targetActivity.startVisibility = targetActivity.endVisibility = -1; |
| |
| targetActivity.transitionComplete = new CountDownLatch(1); |
| |
| mActivity.runOnUiThread(() -> { |
| targetActivity.finishAfterTransition(); |
| }); |
| |
| assertTrue(targetActivity.transitionComplete.await(1, TimeUnit.SECONDS)); |
| assertEquals(View.VISIBLE, targetActivity.startVisibility); |
| assertEquals(View.VISIBLE, targetActivity.endVisibility); |
| |
| assertTrue(targetActivity.transitionComplete.await(1, TimeUnit.SECONDS)); |
| verify(mReenterListener, within(3000)).onTransitionEnd(any()); |
| |
| TargetActivity.sLastCreated = null; |
| } |
| |
| // Starting a shared element transition and then removing the view shouldn't cause problems. |
| @Test |
| public void removeSharedViews() throws Throwable { |
| enterScene(R.layout.scene1); |
| |
| final View redSquare = mActivity.findViewById(R.id.redSquare); |
| final ViewGroup parent = (ViewGroup) redSquare.getParent(); |
| |
| mActivityRule.runOnUiThread(() -> { |
| Bundle options = ActivityOptions.makeSceneTransitionAnimation(mActivity, |
| redSquare, "red").toBundle(); |
| Intent intent = new Intent(mActivity, TargetActivity.class); |
| intent.putExtra(TargetActivity.EXTRA_LAYOUT_ID, R.layout.scene2); |
| intent.putExtra(TargetActivity.EXTRA_USE_ANIMATOR, true); |
| parent.removeView(redSquare); |
| mActivity.startActivity(intent, options); |
| }); |
| |
| |
| TargetActivity targetActivity = waitForTargetActivity(); |
| verify(targetActivity.enterListener, within(3000)).onTransitionEnd(any()); |
| |
| mActivityRule.runOnUiThread(() -> targetActivity.finishAfterTransition()); |
| mActivityRule.runOnUiThread(() -> parent.removeAllViews()); |
| |
| verify(targetActivity.returnListener, times(1)).onTransitionEnd(any()); |
| TargetActivity.sLastCreated = null; |
| } |
| |
| // Ensure that the shared element view copy is the correct image of the shared element view |
| // source |
| @Test |
| public void sharedElementCopied() throws Throwable { |
| enterScene(R.layout.scene1); |
| |
| mActivityRule.runOnUiThread(() -> { |
| View sharedElement = mActivity.findViewById(R.id.redSquare); |
| Bundle options = ActivityOptions.makeSceneTransitionAnimation(mActivity, |
| sharedElement, "red").toBundle(); |
| Intent intent = new Intent(mActivity, TargetActivity.class); |
| intent.putExtra(TargetActivity.EXTRA_LAYOUT_ID, R.layout.scene2); |
| mActivity.startActivity(intent, options); |
| }); |
| |
| TargetActivity targetActivity = waitForTargetActivity(); |
| verify(targetActivity.enterListener, within(3000)).onTransitionEnd(any()); |
| verify(mExitListener, times(1)).onTransitionEnd(any()); |
| |
| final CountDownLatch startCalled = new CountDownLatch(1); |
| final SharedElementCallback sharedElementCallback = new SharedElementCallback() { |
| @Override |
| public void onSharedElementStart(List<String> sharedElementNames, |
| List<View> sharedElements, |
| List<View> sharedElementSnapshots) { |
| int index = sharedElementNames.indexOf("red"); |
| View sharedElement = sharedElementSnapshots.get(index); |
| Drawable backgroundDrawable = sharedElement.getBackground(); |
| BitmapDrawable bitmapDrawable = (BitmapDrawable) backgroundDrawable; |
| Bitmap bitmap = bitmapDrawable.getBitmap(); |
| Bitmap copy = bitmap.copy(Bitmap.Config.ARGB_8888, false); |
| assertEquals(0xFFFF0000, copy.getPixel(1, 1)); |
| startCalled.countDown(); |
| super.onSharedElementStart(sharedElementNames, sharedElements, |
| sharedElementSnapshots); |
| } |
| }; |
| |
| mActivity.setExitSharedElementCallback(sharedElementCallback); |
| mActivityRule.runOnUiThread(() -> targetActivity.finishAfterTransition()); |
| |
| // Should only take a short time, but there's no need to rush it on failure. |
| assertTrue(startCalled.await(5, TimeUnit.SECONDS)); |
| |
| TargetActivity.sLastCreated = null; |
| } |
| |
| private TargetActivity waitForTargetActivity() throws Throwable { |
| PollingCheck.waitFor(() -> TargetActivity.sLastCreated != null); |
| // Just make sure that we're not in the middle of running on the UI thread. |
| mActivityRule.runOnUiThread(() -> { }); |
| return TargetActivity.sLastCreated; |
| } |
| |
| private Set<Integer> getTargetViewIds(TargetTracking transition) { |
| return transition.getTrackedTargets().stream() |
| .map(v -> v.getId()) |
| .collect(Collectors.toSet()); |
| } |
| |
| private void assertTargetContains(TargetTracking transition, int... ids) { |
| Set<Integer> targets = getTargetViewIds(transition); |
| for (int id : ids) { |
| assertTrueWithId(id, "%s was not included from the transition", targets.contains(id)); |
| } |
| } |
| |
| private void assertTargetExcludes(TargetTracking transition, int... ids) { |
| Set<Integer> targets = getTargetViewIds(transition); |
| for (int id : ids) { |
| assertTrueWithId(id, "%s was not excluded from the transition", !targets.contains(id)); |
| } |
| } |
| |
| private void assertTrueWithId(int id, String message, boolean valueToAssert) { |
| if (!valueToAssert) { |
| fail(String.format(message, mActivity.getResources().getResourceName(id))); |
| } |
| } |
| } |