| /* |
| * 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; |
| } |
| } |