1123 lines
44 KiB
Java
1123 lines
44 KiB
Java
/*
|
|
* 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);
|
|
decor.transformMatrixToLocal(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;
|
|
}
|
|
}
|