blob: 4d1756c7c45d12a8526f22065c84c08a836b13e1 [file] [log] [blame]
/*
* 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)));
}
}
}