blob: 4b87a647a80b76583ee55a10d2d99f270e923f92 [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.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.os.ResultReceiver;
import android.transition.Transition;
import android.transition.TransitionListenerAdapter;
import android.transition.TransitionSet;
import android.transition.Visibility;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.view.GhostView;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroupOverlay;
import android.view.ViewParent;
import android.view.ViewRootImpl;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.widget.ImageView;
import com.android.internal.view.OneShotPreDrawListener;
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_LEFT = "shared_element:screenLeft";
protected static final String KEY_SCREEN_TOP = "shared_element:screenTop";
protected static final String KEY_SCREEN_RIGHT = "shared_element:screenRight";
protected static final String KEY_SCREEN_BOTTOM= "shared_element:screenBottom";
protected static final String KEY_TRANSLATION_Z = "shared_element:translationZ";
protected static final String KEY_SNAPSHOT = "shared_element:bitmap";
protected static final String KEY_SCALE_TYPE = "shared_element:scaleType";
protected static final String KEY_IMAGE_MATRIX = "shared_element:imageMatrix";
protected static final String KEY_ELEVATION = "shared_element:elevation";
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 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;
/**
* Sent by Activity#startActivity to notify the entering activity that enter animation for
* back is allowed. If this message is not received, the default exit animation will run when
* backing out of an activity (instead of the 'reverse' shared element transition).
*/
public static final int MSG_ALLOW_RETURN_TRANSITION = 108;
private Window mWindow;
final protected ArrayList<String> mAllSharedElementNames;
final protected ArrayList<View> mSharedElements = new ArrayList<View>();
final protected ArrayList<String> mSharedElementNames = new ArrayList<String>();
protected ArrayList<View> mTransitioningViews = new ArrayList<View>();
protected SharedElementCallback mListener;
protected ResultReceiver mResultReceiver;
final private FixedEpicenterCallback mEpicenterCallback = new FixedEpicenterCallback();
final protected boolean mIsReturning;
private Runnable mPendingTransition;
private boolean mIsStartingTransition;
private ArrayList<GhostViewListeners> mGhostViewListeners =
new ArrayList<GhostViewListeners>();
private ArrayMap<View, Float> mOriginalAlphas = new ArrayMap<View, Float>();
private ArrayList<Matrix> mSharedElementParentMatrices;
private boolean mSharedElementTransitionComplete;
private boolean mViewsTransitionComplete;
private boolean mBackgroundAnimatorComplete;
private ArrayList<View> mStrippedTransitioningViews = new ArrayList<>();
public ActivityTransitionCoordinator(Window window,
ArrayList<String> allSharedElementNames,
SharedElementCallback listener, boolean isReturning) {
super(new Handler());
mWindow = window;
mListener = listener;
mAllSharedElementNames = allSharedElementNames;
mIsReturning = isReturning;
}
protected void viewsReady(ArrayMap<String, View> sharedElements) {
sharedElements.retainAll(mAllSharedElementNames);
if (mListener != null) {
mListener.onMapSharedElements(mAllSharedElementNames, sharedElements);
}
setSharedElements(sharedElements);
if (getViewsTransition() != null && mTransitioningViews != null) {
ViewGroup decorView = getDecor();
if (decorView != null) {
decorView.captureTransitioningViews(mTransitioningViews);
}
mTransitioningViews.removeAll(mSharedElements);
}
setEpicenter();
}
/**
* Iterates over the shared elements and adds them to the members in order.
* Shared elements that are nested in other shared elements are placed after the
* elements that they are nested in. This means that layout ordering can be done
* from first to last.
*
* @param sharedElements The map of transition names to shared elements to set into
* the member fields.
*/
private void setSharedElements(ArrayMap<String, View> sharedElements) {
boolean isFirstRun = true;
while (!sharedElements.isEmpty()) {
final int numSharedElements = sharedElements.size();
for (int i = numSharedElements - 1; i >= 0; i--) {
final View view = sharedElements.valueAt(i);
final String name = sharedElements.keyAt(i);
if (isFirstRun && (view == null || !view.isAttachedToWindow() || name == null)) {
sharedElements.removeAt(i);
} else if (!isNested(view, sharedElements)) {
mSharedElementNames.add(name);
mSharedElements.add(view);
sharedElements.removeAt(i);
}
}
isFirstRun = false;
}
}
/**
* Returns true when view is nested in any of the values of sharedElements.
*/
private static boolean isNested(View view, ArrayMap<String, View> sharedElements) {
ViewParent parent = view.getParent();
boolean isNested = false;
while (parent instanceof View) {
View parentView = (View) parent;
if (sharedElements.containsValue(parentView)) {
isNested = true;
break;
}
parent = parentView.getParent();
}
return isNested;
}
protected void stripOffscreenViews() {
if (mTransitioningViews == null) {
return;
}
Rect r = new Rect();
for (int i = mTransitioningViews.size() - 1; i >= 0; i--) {
View view = mTransitioningViews.get(i);
if (!view.getGlobalVisibleRect(r)) {
mTransitioningViews.remove(i);
mStrippedTransitioningViews.add(view);
}
}
}
protected Window getWindow() {
return mWindow;
}
public 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()) {
int index = mSharedElementNames.indexOf(mAllSharedElementNames.get(0));
if (index >= 0) {
epicenter = mSharedElements.get(index);
}
}
setEpicenter(epicenter);
}
private void setEpicenter(View view) {
if (view == null) {
mEpicenterCallback.setEpicenter(null);
} else {
Rect epicenter = new Rect();
view.getBoundsOnScreen(epicenter);
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).getTransitionName());
}
return names;
}
public ArrayList<View> copyMappedViews() {
return new ArrayList<View>(mSharedElements);
}
protected Transition setTargets(Transition transition, boolean add) {
if (transition == null || (add &&
(mTransitioningViews == null || mTransitioningViews.isEmpty()))) {
return null;
}
// Add the targets to a set containing transition so that transition
// remains unaffected. We don't want to modify the targets of transition itself.
TransitionSet set = new TransitionSet();
if (mTransitioningViews != null) {
for (int i = mTransitioningViews.size() - 1; i >= 0; i--) {
View view = mTransitioningViews.get(i);
if (add) {
set.addTarget(view);
} else {
set.excludeTarget(view, true);
}
}
}
if (mStrippedTransitioningViews != null) {
for (int i = mStrippedTransitioningViews.size() - 1; i >= 0; i--) {
View view = mStrippedTransitioningViews.get(i);
set.excludeTarget(view, true);
}
}
// By adding the transition after addTarget, we prevent addTarget from
// affecting transition.
set.addTransition(transition);
if (!add && mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
// Allow children of excluded transitioning views, but not the views themselves
set = new TransitionSet().addTransition(set);
}
return set;
}
protected Transition configureTransition(Transition transition,
boolean includeTransitioningViews) {
if (transition != null) {
transition = transition.clone();
transition.setEpicenterCallback(mEpicenterCallback);
transition = setTargets(transition, includeTransitioningViews);
}
noLayoutSuppressionForVisibilityTransitions(transition);
return transition;
}
/**
* Looks through the transition to see which Views have been included and which have been
* excluded. {@code views} will be modified to contain only those Views that are included
* in the transition. If {@code transition} is a TransitionSet, it will search through all
* contained Transitions to find targeted Views.
*
* @param transition The transition to look through for inclusion of Views
* @param views The list of Views that are to be checked for inclusion. Will be modified
* to remove all excluded Views, possibly leaving an empty list.
*/
protected static void removeExcludedViews(Transition transition, ArrayList<View> views) {
ArraySet<View> included = new ArraySet<>();
findIncludedViews(transition, views, included);
views.clear();
views.addAll(included);
}
/**
* Looks through the transition to see which Views have been included. Only {@code views}
* will be examined for inclusion. If {@code transition} is a TransitionSet, it will search
* through all contained Transitions to find targeted Views.
*
* @param transition The transition to look through for inclusion of Views
* @param views The list of Views that are to be checked for inclusion.
* @param included Modified to contain all Views in views that have at least one Transition
* that affects it.
*/
private static void findIncludedViews(Transition transition, ArrayList<View> views,
ArraySet<View> included) {
if (transition instanceof TransitionSet) {
TransitionSet set = (TransitionSet) transition;
ArrayList<View> includedViews = new ArrayList<>();
final int numViews = views.size();
for (int i = 0; i < numViews; i++) {
final View view = views.get(i);
if (transition.isValidTarget(view)) {
includedViews.add(view);
}
}
final int count = set.getTransitionCount();
for (int i = 0; i < count; i++) {
findIncludedViews(set.getTransitionAt(i), includedViews, included);
}
} else {
final int numViews = views.size();
for (int i = 0; i < numViews; i++) {
final View view = views.get(i);
if (transition.isValidTarget(view)) {
included.add(view);
}
}
}
}
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;
}
}
protected ArrayMap<String, View> mapSharedElements(ArrayList<String> accepted,
ArrayList<View> localViews) {
ArrayMap<String, View> sharedElements = new ArrayMap<String, View>();
if (accepted != null) {
for (int i = 0; i < accepted.size(); i++) {
sharedElements.put(accepted.get(i), localViews.get(i));
}
} else {
ViewGroup decorView = getDecor();
if (decorView != null) {
decorView.findNamedViews(sharedElements);
}
}
return sharedElements;
}
protected void setResultReceiver(ResultReceiver resultReceiver) {
mResultReceiver = resultReceiver;
}
protected abstract Transition getViewsTransition();
private void setSharedElementState(View view, String name, Bundle transitionArgs,
Matrix tempMatrix, RectF tempRect, int[] decorLoc) {
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);
tempMatrix.setValues(matrixValues);
imageView.setImageMatrix(tempMatrix);
}
}
}
float z = sharedElementBundle.getFloat(KEY_TRANSLATION_Z);
view.setTranslationZ(z);
float elevation = sharedElementBundle.getFloat(KEY_ELEVATION);
view.setElevation(elevation);
float left = sharedElementBundle.getFloat(KEY_SCREEN_LEFT);
float top = sharedElementBundle.getFloat(KEY_SCREEN_TOP);
float right = sharedElementBundle.getFloat(KEY_SCREEN_RIGHT);
float bottom = sharedElementBundle.getFloat(KEY_SCREEN_BOTTOM);
if (decorLoc != null) {
left -= decorLoc[0];
top -= decorLoc[1];
right -= decorLoc[0];
bottom -= decorLoc[1];
} else {
// Find the location in the view's parent
getSharedElementParentMatrix(view, tempMatrix);
tempRect.set(left, top, right, bottom);
tempMatrix.mapRect(tempRect);
float leftInParent = tempRect.left;
float topInParent = tempRect.top;
// Find the size of the view
view.getInverseMatrix().mapRect(tempRect);
float width = tempRect.width();
float height = tempRect.height();
// Now determine the offset due to view transform:
view.setLeft(0);
view.setTop(0);
view.setRight(Math.round(width));
view.setBottom(Math.round(height));
tempRect.set(0, 0, width, height);
view.getMatrix().mapRect(tempRect);
left = leftInParent - tempRect.left;
top = topInParent - tempRect.top;
right = left + width;
bottom = top + height;
}
int x = Math.round(left);
int y = Math.round(top);
int width = Math.round(right) - x;
int height = Math.round(bottom) - y;
int widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);
view.layout(x, y, x + width, y + height);
}
private void setSharedElementMatrices() {
int numSharedElements = mSharedElements.size();
if (numSharedElements > 0) {
mSharedElementParentMatrices = new ArrayList<Matrix>(numSharedElements);
}
for (int i = 0; i < numSharedElements; i++) {
View view = mSharedElements.get(i);
// Find the location in the view's parent
ViewGroup parent = (ViewGroup) view.getParent();
Matrix matrix = new Matrix();
if (parent != null) {
parent.transformMatrixToLocal(matrix);
matrix.postTranslate(parent.getScrollX(), parent.getScrollY());
}
mSharedElementParentMatrices.add(matrix);
}
}
private void getSharedElementParentMatrix(View view, Matrix matrix) {
final int index = mSharedElementParentMatrices == null ? -1
: mSharedElements.indexOf(view);
if (index < 0) {
matrix.reset();
ViewParent viewParent = view.getParent();
if (viewParent instanceof ViewGroup) {
// Find the location in the view's parent
ViewGroup parent = (ViewGroup) viewParent;
parent.transformMatrixToLocal(matrix);
matrix.postTranslate(parent.getScrollX(), parent.getScrollY());
}
} else {
// The indices of mSharedElementParentMatrices matches the
// mSharedElement matrices.
Matrix parentMatrix = mSharedElementParentMatrices.get(index);
matrix.set(parentMatrix);
}
}
protected ArrayList<SharedElementOriginalState> setSharedElementState(
Bundle sharedElementState, final ArrayList<View> snapshots) {
ArrayList<SharedElementOriginalState> originalImageState =
new ArrayList<SharedElementOriginalState>();
if (sharedElementState != null) {
Matrix tempMatrix = new Matrix();
RectF tempRect = new RectF();
final int numSharedElements = mSharedElements.size();
for (int i = 0; i < numSharedElements; i++) {
View sharedElement = mSharedElements.get(i);
String name = mSharedElementNames.get(i);
SharedElementOriginalState originalState = getOldSharedElementState(sharedElement,
name, sharedElementState);
originalImageState.add(originalState);
setSharedElementState(sharedElement, name, sharedElementState,
tempMatrix, tempRect, null);
}
}
if (mListener != null) {
mListener.onSharedElementStart(mSharedElementNames, mSharedElements, snapshots);
}
return originalImageState;
}
protected void notifySharedElementEnd(ArrayList<View> snapshots) {
if (mListener != null) {
mListener.onSharedElementEnd(mSharedElementNames, mSharedElements, snapshots);
}
}
protected void scheduleSetSharedElementEnd(final ArrayList<View> snapshots) {
final View decorView = getDecor();
if (decorView != null) {
OneShotPreDrawListener.add(decorView, () -> {
notifySharedElementEnd(snapshots);
});
}
}
private static SharedElementOriginalState getOldSharedElementState(View view, String name,
Bundle transitionArgs) {
SharedElementOriginalState state = new SharedElementOriginalState();
state.mLeft = view.getLeft();
state.mTop = view.getTop();
state.mRight = view.getRight();
state.mBottom = view.getBottom();
state.mMeasuredWidth = view.getMeasuredWidth();
state.mMeasuredHeight = view.getMeasuredHeight();
state.mTranslationZ = view.getTranslationZ();
state.mElevation = view.getElevation();
if (!(view instanceof ImageView)) {
return state;
}
Bundle bundle = transitionArgs.getBundle(name);
if (bundle == null) {
return state;
}
int scaleTypeInt = bundle.getInt(KEY_SCALE_TYPE, -1);
if (scaleTypeInt < 0) {
return state;
}
ImageView imageView = (ImageView) view;
state.mScaleType = imageView.getScaleType();
if (state.mScaleType == ImageView.ScaleType.MATRIX) {
state.mMatrix = new Matrix(imageView.getImageMatrix());
}
return state;
}
protected ArrayList<View> createSnapshots(Bundle state, Collection<String> names) {
int numSharedElements = names.size();
ArrayList<View> snapshots = new ArrayList<View>(numSharedElements);
if (numSharedElements == 0) {
return snapshots;
}
Context context = getWindow().getContext();
int[] decorLoc = new int[2];
ViewGroup decorView = getDecor();
if (decorView != null) {
decorView.getLocationOnScreen(decorLoc);
}
Matrix tempMatrix = new Matrix();
for (String name: names) {
Bundle sharedElementBundle = state.getBundle(name);
View snapshot = null;
if (sharedElementBundle != null) {
Parcelable parcelable = sharedElementBundle.getParcelable(KEY_SNAPSHOT);
if (parcelable != null && mListener != null) {
snapshot = mListener.onCreateSnapshotView(context, parcelable);
}
if (snapshot != null) {
setSharedElementState(snapshot, name, state, tempMatrix, null, decorLoc);
}
}
// Even null snapshots are added so they remain in the same order as shared elements.
snapshots.add(snapshot);
}
return snapshots;
}
protected static void setOriginalSharedElementState(ArrayList<View> sharedElements,
ArrayList<SharedElementOriginalState> originalState) {
for (int i = 0; i < originalState.size(); i++) {
View view = sharedElements.get(i);
SharedElementOriginalState state = originalState.get(i);
if (view instanceof ImageView && state.mScaleType != null) {
ImageView imageView = (ImageView) view;
imageView.setScaleType(state.mScaleType);
if (state.mScaleType == ImageView.ScaleType.MATRIX) {
imageView.setImageMatrix(state.mMatrix);
}
}
view.setElevation(state.mElevation);
view.setTranslationZ(state.mTranslationZ);
int widthSpec = View.MeasureSpec.makeMeasureSpec(state.mMeasuredWidth,
View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(state.mMeasuredHeight,
View.MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);
view.layout(state.mLeft, state.mTop, state.mRight, state.mBottom);
}
}
protected Bundle captureSharedElementState() {
Bundle bundle = new Bundle();
RectF tempBounds = new RectF();
Matrix tempMatrix = new Matrix();
for (int i = 0; i < mSharedElements.size(); i++) {
View sharedElement = mSharedElements.get(i);
String name = mSharedElementNames.get(i);
captureSharedElementState(sharedElement, name, bundle, tempMatrix, tempBounds);
}
return bundle;
}
protected void clearState() {
// Clear the state so that we can't hold any references accidentally and leak memory.
mWindow = null;
mSharedElements.clear();
mTransitioningViews = null;
mStrippedTransitioningViews = null;
mOriginalAlphas.clear();
mResultReceiver = null;
mPendingTransition = null;
mListener = null;
mSharedElementParentMatrices = null;
}
protected long getFadeDuration() {
return getWindow().getTransitionBackgroundFadeDuration();
}
protected void hideViews(ArrayList<View> views) {
int count = views.size();
for (int i = 0; i < count; i++) {
View view = views.get(i);
if (!mOriginalAlphas.containsKey(view)) {
mOriginalAlphas.put(view, view.getAlpha());
}
view.setAlpha(0f);
}
}
protected void showViews(ArrayList<View> views, boolean setTransitionAlpha) {
int count = views.size();
for (int i = 0; i < count; i++) {
showView(views.get(i), setTransitionAlpha);
}
}
private void showView(View view, boolean setTransitionAlpha) {
Float alpha = mOriginalAlphas.remove(view);
if (alpha != null) {
view.setAlpha(alpha);
}
if (setTransitionAlpha) {
view.setTransitionAlpha(1f);
}
}
/**
* 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 tempBounds A temporary Rect for capturing the current location of views.
*/
protected void captureSharedElementState(View view, String name, Bundle transitionArgs,
Matrix tempMatrix, RectF tempBounds) {
Bundle sharedElementBundle = new Bundle();
tempMatrix.reset();
view.transformMatrixToGlobal(tempMatrix);
tempBounds.set(0, 0, view.getWidth(), view.getHeight());
tempMatrix.mapRect(tempBounds);
sharedElementBundle.putFloat(KEY_SCREEN_LEFT, tempBounds.left);
sharedElementBundle.putFloat(KEY_SCREEN_RIGHT, tempBounds.right);
sharedElementBundle.putFloat(KEY_SCREEN_TOP, tempBounds.top);
sharedElementBundle.putFloat(KEY_SCREEN_BOTTOM, tempBounds.bottom);
sharedElementBundle.putFloat(KEY_TRANSLATION_Z, view.getTranslationZ());
sharedElementBundle.putFloat(KEY_ELEVATION, view.getElevation());
Parcelable bitmap = null;
if (mListener != null) {
bitmap = mListener.onCaptureSharedElementSnapshot(view, tempMatrix, tempBounds);
}
if (bitmap != null) {
sharedElementBundle.putParcelable(KEY_SNAPSHOT, 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);
}
protected void startTransition(Runnable runnable) {
if (mIsStartingTransition) {
mPendingTransition = runnable;
} else {
mIsStartingTransition = true;
runnable.run();
}
}
protected void transitionStarted() {
mIsStartingTransition = false;
}
/**
* Cancels any pending transitions and returns true if there is a transition is in
* the middle of starting.
*/
protected boolean cancelPendingTransitions() {
mPendingTransition = null;
return mIsStartingTransition;
}
protected void moveSharedElementsToOverlay() {
if (mWindow == null || !mWindow.getSharedElementsUseOverlay()) {
return;
}
setSharedElementMatrices();
int numSharedElements = mSharedElements.size();
ViewGroup decor = getDecor();
if (decor != null) {
boolean moveWithParent = moveSharedElementWithParent();
Matrix tempMatrix = new Matrix();
for (int i = 0; i < numSharedElements; i++) {
View view = mSharedElements.get(i);
if (view.isAttachedToWindow()) {
tempMatrix.reset();
mSharedElementParentMatrices.get(i).invert(tempMatrix);
GhostView.addGhost(view, decor, tempMatrix);
ViewGroup parent = (ViewGroup) view.getParent();
if (moveWithParent && !isInTransitionGroup(parent, decor)) {
GhostViewListeners listener = new GhostViewListeners(view, parent, decor);
parent.getViewTreeObserver().addOnPreDrawListener(listener);
parent.addOnAttachStateChangeListener(listener);
mGhostViewListeners.add(listener);
}
}
}
}
}
protected boolean moveSharedElementWithParent() {
return true;
}
public static boolean isInTransitionGroup(ViewParent viewParent, ViewGroup decor) {
if (viewParent == decor || !(viewParent instanceof ViewGroup)) {
return false;
}
ViewGroup parent = (ViewGroup) viewParent;
if (parent.isTransitionGroup()) {
return true;
} else {
return isInTransitionGroup(parent.getParent(), decor);
}
}
protected void moveSharedElementsFromOverlay() {
int numListeners = mGhostViewListeners.size();
for (int i = 0; i < numListeners; i++) {
GhostViewListeners listener = mGhostViewListeners.get(i);
listener.removeListener();
}
mGhostViewListeners.clear();
if (mWindow == null || !mWindow.getSharedElementsUseOverlay()) {
return;
}
ViewGroup decor = getDecor();
if (decor != null) {
ViewGroupOverlay overlay = decor.getOverlay();
int count = mSharedElements.size();
for (int i = 0; i < count; i++) {
View sharedElement = mSharedElements.get(i);
GhostView.removeGhost(sharedElement);
}
}
}
protected void setGhostVisibility(int visibility) {
int numSharedElements = mSharedElements.size();
for (int i = 0; i < numSharedElements; i++) {
GhostView ghostView = GhostView.getGhost(mSharedElements.get(i));
if (ghostView != null) {
ghostView.setVisibility(visibility);
}
}
}
protected void scheduleGhostVisibilityChange(final int visibility) {
final View decorView = getDecor();
if (decorView != null) {
OneShotPreDrawListener.add(decorView, () -> {
setGhostVisibility(visibility);
});
}
}
protected boolean isViewsTransitionComplete() {
return mViewsTransitionComplete;
}
protected void viewsTransitionComplete() {
mViewsTransitionComplete = true;
startInputWhenTransitionsComplete();
}
protected void backgroundAnimatorComplete() {
mBackgroundAnimatorComplete = true;
}
protected void sharedElementTransitionComplete() {
mSharedElementTransitionComplete = true;
startInputWhenTransitionsComplete();
}
private void startInputWhenTransitionsComplete() {
if (mViewsTransitionComplete && mSharedElementTransitionComplete) {
final View decor = getDecor();
if (decor != null) {
final ViewRootImpl viewRoot = decor.getViewRootImpl();
if (viewRoot != null) {
viewRoot.setPausedForTransition(false);
}
}
onTransitionsComplete();
}
}
protected void pauseInput() {
final View decor = getDecor();
final ViewRootImpl viewRoot = decor == null ? null : decor.getViewRootImpl();
if (viewRoot != null) {
viewRoot.setPausedForTransition(true);
}
}
protected void onTransitionsComplete() {}
protected class ContinueTransitionListener extends TransitionListenerAdapter {
@Override
public void onTransitionStart(Transition transition) {
mIsStartingTransition = false;
Runnable pending = mPendingTransition;
mPendingTransition = null;
if (pending != null) {
startTransition(pending);
}
}
@Override
public void onTransitionEnd(Transition transition) {
transition.removeListener(this);
}
}
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;
}
protected void setTransitioningViewsVisiblity(int visiblity, boolean invalidate) {
final int numElements = mTransitioningViews == null ? 0 : mTransitioningViews.size();
for (int i = 0; i < numElements; i++) {
final View view = mTransitioningViews.get(i);
if (invalidate) {
// Allow the view to be invalidated by the visibility change
view.setVisibility(visiblity);
} else {
// Don't invalidate the view with the visibility change
view.setTransitionVisibility(visiblity);
}
}
}
/**
* Blocks suppressLayout from Visibility transitions. It is ok to suppress the layout,
* but we don't want to force the layout when suppressLayout becomes false. This leads
* to visual glitches.
*/
private static void noLayoutSuppressionForVisibilityTransitions(Transition transition) {
if (transition instanceof Visibility) {
final Visibility visibility = (Visibility) transition;
visibility.setSuppressLayout(false);
} else if (transition instanceof TransitionSet) {
final TransitionSet set = (TransitionSet) transition;
final int count = set.getTransitionCount();
for (int i = 0; i < count; i++) {
noLayoutSuppressionForVisibilityTransitions(set.getTransitionAt(i));
}
}
}
public boolean isTransitionRunning() {
return !(mViewsTransitionComplete && mSharedElementTransitionComplete &&
mBackgroundAnimatorComplete);
}
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;
}
}
private static class GhostViewListeners implements ViewTreeObserver.OnPreDrawListener,
View.OnAttachStateChangeListener {
private View mView;
private ViewGroup mDecor;
private View mParent;
private Matrix mMatrix = new Matrix();
private ViewTreeObserver mViewTreeObserver;
public GhostViewListeners(View view, View parent, ViewGroup decor) {
mView = view;
mParent = parent;
mDecor = decor;
mViewTreeObserver = parent.getViewTreeObserver();
}
public View getView() {
return mView;
}
@Override
public boolean onPreDraw() {
GhostView ghostView = GhostView.getGhost(mView);
if (ghostView == null || !mView.isAttachedToWindow()) {
removeListener();
} else {
GhostView.calculateMatrix(mView, mDecor, mMatrix);
ghostView.setMatrix(mMatrix);
}
return true;
}
public void removeListener() {
if (mViewTreeObserver.isAlive()) {
mViewTreeObserver.removeOnPreDrawListener(this);
} else {
mParent.getViewTreeObserver().removeOnPreDrawListener(this);
}
mParent.removeOnAttachStateChangeListener(this);
}
@Override
public void onViewAttachedToWindow(View v) {
mViewTreeObserver = v.getViewTreeObserver();
}
@Override
public void onViewDetachedFromWindow(View v) {
removeListener();
}
}
static class SharedElementOriginalState {
int mLeft;
int mTop;
int mRight;
int mBottom;
int mMeasuredWidth;
int mMeasuredHeight;
ImageView.ScaleType mScaleType;
Matrix mMatrix;
float mTranslationZ;
float mElevation;
}
}