blob: 703df51a60183737b7d5bfd26c677dce38c98c52 [file] [log] [blame]
/*
* Copyright (C) 2014 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.app;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.ResultReceiver;
import android.transition.Transition;
import android.transition.TransitionSet;
import android.util.ArrayMap;
import android.util.Pair;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.widget.ImageView;
import java.util.ArrayList;
import java.util.Collection;
/**
* Base class for ExitTransitionCoordinator and EnterTransitionCoordinator, classes
* that manage activity transitions and the communications coordinating them between
* Activities. The ExitTransitionCoordinator is created in the
* ActivityOptions#makeSceneTransitionAnimation. The EnterTransitionCoordinator
* is created by ActivityOptions#createEnterActivityTransition by Activity when the window is
* attached.
*
* Typical startActivity goes like this:
* 1) ExitTransitionCoordinator created with ActivityOptions#makeSceneTransitionAnimation
* 2) Activity#startActivity called and that calls startExit() through
* ActivityOptions#dispatchStartExit
* - Exit transition starts by setting transitioning Views to INVISIBLE
* 3) Launched Activity starts, creating an EnterTransitionCoordinator.
* - The Window is made translucent
* - The Window background alpha is set to 0
* - The transitioning views are made INVISIBLE
* - MSG_SET_LISTENER is sent back to the ExitTransitionCoordinator.
* 4) The shared element transition completes.
* - MSG_TAKE_SHARED_ELEMENTS is sent to the EnterTransitionCoordinator
* 5) The MSG_TAKE_SHARED_ELEMENTS is received by the EnterTransitionCoordinator.
* - Shared elements are made VISIBLE
* - Shared elements positions and size are set to match the end state of the calling
* Activity.
* - The shared element transition is started
* - If the window allows overlapping transitions, the views transition is started by setting
* the entering Views to VISIBLE and the background alpha is animated to opaque.
* - MSG_HIDE_SHARED_ELEMENTS is sent to the ExitTransitionCoordinator
* 6) MSG_HIDE_SHARED_ELEMENTS is received by the ExitTransitionCoordinator
* - The shared elements are made INVISIBLE
* 7) The exit transition completes in the calling Activity.
* - MSG_EXIT_TRANSITION_COMPLETE is sent to the EnterTransitionCoordinator.
* 8) The MSG_EXIT_TRANSITION_COMPLETE is received by the EnterTransitionCoordinator.
* - If the window doesn't allow overlapping enter transitions, the enter transition is started
* by setting entering views to VISIBLE and the background is animated to opaque.
* 9) The background opacity animation completes.
* - The window is made opaque
* 10) The calling Activity gets an onStop() call
* - onActivityStopped() is called and all exited Views are made VISIBLE.
*
* Typical finishAfterTransition goes like this:
* 1) finishAfterTransition() creates an ExitTransitionCoordinator and calls startExit()
* - The Window start transitioning to Translucent with a new ActivityOptions.
* - If no background exists, a black background is substituted
* - The shared elements in the scene are matched against those shared elements
* that were sent by comparing the names.
* - The exit transition is started by setting Views to INVISIBLE.
* 2) The ActivityOptions is received by the Activity and an EnterTransitionCoordinator is created.
* - All transitioning views are made VISIBLE to reverse what was done when onActivityStopped()
* was called
* 3) The Window is made translucent and a callback is received
* - The background alpha is animated to 0
* 4) The background alpha animation completes
* 5) The shared element transition completes
* - After both 4 & 5 complete, MSG_TAKE_SHARED_ELEMENTS is sent to the
* EnterTransitionCoordinator
* 6) MSG_TAKE_SHARED_ELEMENTS is received by EnterTransitionCoordinator
* - Shared elements are made VISIBLE
* - Shared elements positions and size are set to match the end state of the calling
* Activity.
* - The shared element transition is started
* - If the window allows overlapping transitions, the views transition is started by setting
* the entering Views to VISIBLE.
* - MSG_HIDE_SHARED_ELEMENTS is sent to the ExitTransitionCoordinator
* 7) MSG_HIDE_SHARED_ELEMENTS is received by the ExitTransitionCoordinator
* - The shared elements are made INVISIBLE
* 8) The exit transition completes in the finishing Activity.
* - MSG_EXIT_TRANSITION_COMPLETE is sent to the EnterTransitionCoordinator.
* - finish() is called on the exiting Activity
* 9) The MSG_EXIT_TRANSITION_COMPLETE is received by the EnterTransitionCoordinator.
* - If the window doesn't allow overlapping enter transitions, the enter transition is started
* by setting entering views to VISIBLE.
*/
abstract class ActivityTransitionCoordinator extends ResultReceiver {
private static final String TAG = "ActivityTransitionCoordinator";
/**
* For Activity transitions, the called Activity's listener to receive calls
* when transitions complete.
*/
static final String KEY_REMOTE_RECEIVER = "android:remoteReceiver";
protected static final String KEY_SCREEN_X = "shared_element:screenX";
protected static final String KEY_SCREEN_Y = "shared_element:screenY";
protected static final String KEY_TRANSLATION_Z = "shared_element:translationZ";
protected static final String KEY_WIDTH = "shared_element:width";
protected static final String KEY_HEIGHT = "shared_element:height";
protected static final String KEY_BITMAP = "shared_element:bitmap";
protected static final String KEY_SCALE_TYPE = "shared_element:scaleType";
protected static final String KEY_IMAGE_MATRIX = "shared_element:imageMatrix";
// The background fade in/out duration. TODO: Enable tuning this.
public static final int FADE_BACKGROUND_DURATION_MS = 300;
protected static final ImageView.ScaleType[] SCALE_TYPE_VALUES = ImageView.ScaleType.values();
/**
* Sent by the exiting coordinator (either EnterTransitionCoordinator
* or ExitTransitionCoordinator) after the shared elements have
* become stationary (shared element transition completes). This tells
* the remote coordinator to take control of the shared elements and
* that animations may begin. The remote Activity won't start entering
* until this message is received, but may wait for
* MSG_EXIT_TRANSITION_COMPLETE if allowOverlappingTransitions() is true.
*/
public static final int MSG_SET_REMOTE_RECEIVER = 100;
/**
* Sent by the entering coordinator to tell the exiting coordinator
* to hide its shared elements after it has started its shared
* element transition. This is temporary until the
* interlock of shared elements is figured out.
*/
public static final int MSG_HIDE_SHARED_ELEMENTS = 101;
/**
* Sent by the exiting Activity in ActivityOptions#dispatchActivityStopped
* to leave the Activity in a good state after it has been hidden.
*/
public static final int MSG_ACTIVITY_STOPPED = 102;
/**
* Sent by the exiting coordinator (either EnterTransitionCoordinator
* or ExitTransitionCoordinator) after the shared elements have
* become stationary (shared element transition completes). This tells
* the remote coordinator to take control of the shared elements and
* that animations may begin. The remote Activity won't start entering
* until this message is received, but may wait for
* MSG_EXIT_TRANSITION_COMPLETE if allowOverlappingTransitions() is true.
*/
public static final int MSG_TAKE_SHARED_ELEMENTS = 103;
/**
* Sent by the exiting coordinator (either
* EnterTransitionCoordinator or ExitTransitionCoordinator) after
* the exiting Views have finished leaving the scene. This will
* be ignored if allowOverlappingTransitions() is true on the
* remote coordinator. If it is false, it will trigger the enter
* transition to start.
*/
public static final int MSG_EXIT_TRANSITION_COMPLETE = 104;
/**
* Sent by Activity#startActivity to begin the exit transition.
*/
public static final int MSG_START_EXIT_TRANSITION = 105;
/**
* It took too long for a message from the entering Activity, so we canceled the transition.
*/
public static final int MSG_CANCEL = 106;
/**
* When returning, this is the destination location for the shared element.
*/
public static final int MSG_SHARED_ELEMENT_DESTINATION = 107;
/**
* Send the shared element positions.
*/
public static final int MSG_SEND_SHARED_ELEMENT_DESTINATION = 108;
final private Window mWindow;
final protected ArrayList<String> mAllSharedElementNames;
final protected ArrayList<View> mSharedElements = new ArrayList<View>();
final protected ArrayList<String> mSharedElementNames = new ArrayList<String>();
final protected ArrayList<View> mTransitioningViews = new ArrayList<View>();
final protected SharedElementListener mListener;
protected ResultReceiver mResultReceiver;
final private FixedEpicenterCallback mEpicenterCallback = new FixedEpicenterCallback();
final protected boolean mIsReturning;
public ActivityTransitionCoordinator(Window window,
ArrayList<String> allSharedElementNames,
ArrayList<String> accepted, ArrayList<String> localNames,
SharedElementListener listener, boolean isReturning) {
this(window, allSharedElementNames, listener, isReturning);
viewsReady(accepted, localNames);
}
public ActivityTransitionCoordinator(Window window,
ArrayList<String> allSharedElementNames,
SharedElementListener listener, boolean isReturning) {
super(new Handler());
mWindow = window;
mListener = listener;
mAllSharedElementNames = allSharedElementNames;
mIsReturning = isReturning;
}
protected void viewsReady(ArrayList<String> accepted, ArrayList<String> localNames) {
setSharedElements(accepted, localNames);
if (getViewsTransition() != null) {
getDecor().captureTransitioningViews(mTransitioningViews);
mTransitioningViews.removeAll(mSharedElements);
}
setEpicenter();
}
protected Window getWindow() {
return mWindow;
}
protected ViewGroup getDecor() {
return (mWindow == null) ? null : (ViewGroup) mWindow.getDecorView();
}
/**
* Sets the transition epicenter to the position of the first shared element.
*/
protected void setEpicenter() {
View epicenter = null;
if (!mAllSharedElementNames.isEmpty() && !mSharedElementNames.isEmpty() &&
mAllSharedElementNames.get(0).equals(mSharedElementNames.get(0))) {
epicenter = mSharedElements.get(0);
}
setEpicenter(epicenter);
}
private void setEpicenter(View view) {
if (view == null) {
mEpicenterCallback.setEpicenter(null);
} else {
int[] loc = new int[2];
view.getLocationOnScreen(loc);
int left = loc[0] + Math.round(view.getTranslationX());
int top = loc[1] + Math.round(view.getTranslationY());
int right = left + view.getWidth();
int bottom = top + view.getHeight();
Rect epicenter = new Rect(left, top, right, bottom);
mEpicenterCallback.setEpicenter(epicenter);
}
}
public ArrayList<String> getAcceptedNames() {
return mSharedElementNames;
}
public ArrayList<String> getMappedNames() {
ArrayList<String> names = new ArrayList<String>(mSharedElements.size());
for (int i = 0; i < mSharedElements.size(); i++) {
names.add(mSharedElements.get(i).getViewName());
}
return names;
}
public ArrayList<String> getAllSharedElementNames() { return mAllSharedElementNames; }
public static void setViewVisibility(Collection<View> views, int visibility) {
if (views != null) {
for (View view : views) {
view.setVisibility(visibility);
}
}
}
protected static Transition addTargets(Transition transition, Collection<View> views) {
if (transition == null || views == null || views.isEmpty()) {
return null;
}
TransitionSet set = new TransitionSet();
set.addTransition(transition);
if (views != null) {
for (View view: views) {
set.addTarget(view);
}
}
return set;
}
protected Transition configureTransition(Transition transition) {
if (transition != null) {
transition = transition.clone();
transition.setEpicenterCallback(mEpicenterCallback);
}
return transition;
}
protected static Transition mergeTransitions(Transition transition1, Transition transition2) {
if (transition1 == null) {
return transition2;
} else if (transition2 == null) {
return transition1;
} else {
TransitionSet transitionSet = new TransitionSet();
transitionSet.addTransition(transition1);
transitionSet.addTransition(transition2);
return transitionSet;
}
}
private void setSharedElements(ArrayList<String> accepted, ArrayList<String> localNames) {
if (!mAllSharedElementNames.isEmpty()) {
ArrayMap<String, View> sharedElements = new ArrayMap<String, View>();
getDecor().findNamedViews(sharedElements);
if (accepted != null) {
for (int i = 0; i < localNames.size(); i++) {
String localName = localNames.get(i);
String acceptedName = accepted.get(i);
if (!localName.equals(acceptedName)) {
View view = sharedElements.remove(localName);
if (view != null) {
sharedElements.put(acceptedName, view);
}
}
}
}
sharedElements.retainAll(mAllSharedElementNames);
mListener.remapSharedElements(mAllSharedElementNames, sharedElements);
sharedElements.retainAll(mAllSharedElementNames);
for (int i = 0; i < mAllSharedElementNames.size(); i++) {
String name = mAllSharedElementNames.get(i);
View sharedElement = sharedElements.get(name);
if (sharedElement != null) {
mSharedElementNames.add(name);
mSharedElements.add(sharedElement);
}
}
}
}
protected void setResultReceiver(ResultReceiver resultReceiver) {
mResultReceiver = resultReceiver;
}
protected abstract Transition getViewsTransition();
private static void setSharedElementState(View view, String name, Bundle transitionArgs,
int[] parentLoc) {
Bundle sharedElementBundle = transitionArgs.getBundle(name);
if (sharedElementBundle == null) {
return;
}
if (view instanceof ImageView) {
int scaleTypeInt = sharedElementBundle.getInt(KEY_SCALE_TYPE, -1);
if (scaleTypeInt >= 0) {
ImageView imageView = (ImageView) view;
ImageView.ScaleType scaleType = SCALE_TYPE_VALUES[scaleTypeInt];
imageView.setScaleType(scaleType);
if (scaleType == ImageView.ScaleType.MATRIX) {
float[] matrixValues = sharedElementBundle.getFloatArray(KEY_IMAGE_MATRIX);
Matrix matrix = new Matrix();
matrix.setValues(matrixValues);
imageView.setImageMatrix(matrix);
}
}
}
float z = sharedElementBundle.getFloat(KEY_TRANSLATION_Z);
view.setTranslationZ(z);
int x = sharedElementBundle.getInt(KEY_SCREEN_X);
int y = sharedElementBundle.getInt(KEY_SCREEN_Y);
int width = sharedElementBundle.getInt(KEY_WIDTH);
int height = sharedElementBundle.getInt(KEY_HEIGHT);
int widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);
int left = x - parentLoc[0];
int top = y - parentLoc[1];
int right = left + width;
int bottom = top + height;
view.layout(left, top, right, bottom);
}
protected ArrayMap<ImageView, Pair<ImageView.ScaleType, Matrix>> setSharedElementState(
Bundle sharedElementState, final ArrayList<View> snapshots) {
ArrayMap<ImageView, Pair<ImageView.ScaleType, Matrix>> originalImageState =
new ArrayMap<ImageView, Pair<ImageView.ScaleType, Matrix>>();
if (sharedElementState != null) {
int[] tempLoc = new int[2];
for (int i = 0; i < mSharedElementNames.size(); i++) {
View sharedElement = mSharedElements.get(i);
String name = mSharedElementNames.get(i);
Pair<ImageView.ScaleType, Matrix> originalState = getOldImageState(sharedElement,
name, sharedElementState);
if (originalState != null) {
originalImageState.put((ImageView) sharedElement, originalState);
}
View parent = (View) sharedElement.getParent();
parent.getLocationOnScreen(tempLoc);
setSharedElementState(sharedElement, name, sharedElementState, tempLoc);
}
}
mListener.setSharedElementStart(mSharedElementNames, mSharedElements, snapshots);
getDecor().getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
getDecor().getViewTreeObserver().removeOnPreDrawListener(this);
mListener.setSharedElementEnd(mSharedElementNames, mSharedElements,
snapshots);
return true;
}
}
);
return originalImageState;
}
private static Pair<ImageView.ScaleType, Matrix> getOldImageState(View view, String name,
Bundle transitionArgs) {
if (!(view instanceof ImageView)) {
return null;
}
Bundle bundle = transitionArgs.getBundle(name);
if (bundle == null) {
return null;
}
int scaleTypeInt = bundle.getInt(KEY_SCALE_TYPE, -1);
if (scaleTypeInt < 0) {
return null;
}
ImageView imageView = (ImageView) view;
ImageView.ScaleType originalScaleType = imageView.getScaleType();
Matrix originalMatrix = null;
if (originalScaleType == ImageView.ScaleType.MATRIX) {
originalMatrix = new Matrix(imageView.getImageMatrix());
}
return Pair.create(originalScaleType, originalMatrix);
}
protected ArrayList<View> createSnapshots(Bundle state, Collection<String> names) {
int numSharedElements = names.size();
if (numSharedElements == 0) {
return null;
}
ArrayList<View> snapshots = new ArrayList<View>(numSharedElements);
Context context = getWindow().getContext();
int[] parentLoc = new int[2];
getDecor().getLocationOnScreen(parentLoc);
for (String name: names) {
Bundle sharedElementBundle = state.getBundle(name);
if (sharedElementBundle != null) {
Bitmap bitmap = sharedElementBundle.getParcelable(KEY_BITMAP);
View snapshot = new View(context);
Resources resources = getWindow().getContext().getResources();
if (bitmap != null) {
snapshot.setBackground(new BitmapDrawable(resources, bitmap));
}
snapshot.setViewName(name);
setSharedElementState(snapshot, name, state, parentLoc);
snapshots.add(snapshot);
}
}
return snapshots;
}
protected static void setOriginalImageViewState(
ArrayMap<ImageView, Pair<ImageView.ScaleType, Matrix>> originalState) {
for (int i = 0; i < originalState.size(); i++) {
ImageView imageView = originalState.keyAt(i);
Pair<ImageView.ScaleType, Matrix> state = originalState.valueAt(i);
imageView.setScaleType(state.first);
imageView.setImageMatrix(state.second);
}
}
protected Bundle captureSharedElementState() {
Bundle bundle = new Bundle();
int[] tempLoc = new int[2];
for (int i = 0; i < mSharedElementNames.size(); i++) {
View sharedElement = mSharedElements.get(i);
String name = mSharedElementNames.get(i);
captureSharedElementState(sharedElement, name, bundle, tempLoc);
}
return bundle;
}
/**
* Captures placement information for Views with a shared element name for
* Activity Transitions.
*
* @param view The View to capture the placement information for.
* @param name The shared element name in the target Activity to apply the placement
* information for.
* @param transitionArgs Bundle to store shared element placement information.
* @param tempLoc A temporary int[2] for capturing the current location of views.
*/
private static void captureSharedElementState(View view, String name, Bundle transitionArgs,
int[] tempLoc) {
Bundle sharedElementBundle = new Bundle();
view.getLocationOnScreen(tempLoc);
float scaleX = view.getScaleX();
sharedElementBundle.putInt(KEY_SCREEN_X, tempLoc[0]);
int width = Math.round(view.getWidth() * scaleX);
sharedElementBundle.putInt(KEY_WIDTH, width);
float scaleY = view.getScaleY();
sharedElementBundle.putInt(KEY_SCREEN_Y, tempLoc[1]);
int height = Math.round(view.getHeight() * scaleY);
sharedElementBundle.putInt(KEY_HEIGHT, height);
sharedElementBundle.putFloat(KEY_TRANSLATION_Z, view.getTranslationZ());
if (width > 0 && height > 0) {
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
view.draw(canvas);
sharedElementBundle.putParcelable(KEY_BITMAP, bitmap);
}
if (view instanceof ImageView) {
ImageView imageView = (ImageView) view;
int scaleTypeInt = scaleTypeToInt(imageView.getScaleType());
sharedElementBundle.putInt(KEY_SCALE_TYPE, scaleTypeInt);
if (imageView.getScaleType() == ImageView.ScaleType.MATRIX) {
float[] matrix = new float[9];
imageView.getImageMatrix().getValues(matrix);
sharedElementBundle.putFloatArray(KEY_IMAGE_MATRIX, matrix);
}
}
transitionArgs.putBundle(name, sharedElementBundle);
}
private static int scaleTypeToInt(ImageView.ScaleType scaleType) {
for (int i = 0; i < SCALE_TYPE_VALUES.length; i++) {
if (scaleType == SCALE_TYPE_VALUES[i]) {
return i;
}
}
return -1;
}
private static class FixedEpicenterCallback extends Transition.EpicenterCallback {
private Rect mEpicenter;
public void setEpicenter(Rect epicenter) { mEpicenter = epicenter; }
@Override
public Rect onGetEpicenter(Transition transition) {
return mEpicenter;
}
}
}