565 lines
20 KiB
Java
565 lines
20 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.animation.Animator;
|
||
|
import android.animation.AnimatorListenerAdapter;
|
||
|
import android.animation.ObjectAnimator;
|
||
|
import android.annotation.NonNull;
|
||
|
import android.app.SharedElementCallback.OnSharedElementsReadyListener;
|
||
|
import android.content.Intent;
|
||
|
import android.graphics.Color;
|
||
|
import android.graphics.Matrix;
|
||
|
import android.graphics.RectF;
|
||
|
import android.graphics.drawable.ColorDrawable;
|
||
|
import android.graphics.drawable.Drawable;
|
||
|
import android.os.Build.VERSION_CODES;
|
||
|
import android.os.Bundle;
|
||
|
import android.os.Handler;
|
||
|
import android.os.Message;
|
||
|
import android.os.ResultReceiver;
|
||
|
import android.transition.Transition;
|
||
|
import android.transition.TransitionListenerAdapter;
|
||
|
import android.transition.TransitionManager;
|
||
|
import android.view.View;
|
||
|
import android.view.ViewGroup;
|
||
|
import android.view.Window;
|
||
|
|
||
|
import com.android.internal.view.OneShotPreDrawListener;
|
||
|
|
||
|
import java.util.ArrayList;
|
||
|
|
||
|
/**
|
||
|
* This ActivityTransitionCoordinator is created in ActivityOptions#makeSceneTransitionAnimation
|
||
|
* to govern the exit of the Scene and the shared elements when calling an Activity as well as
|
||
|
* the reentry of the Scene when coming back from the called Activity.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public class ExitTransitionCoordinator extends ActivityTransitionCoordinator {
|
||
|
private static final String TAG = "ExitTransitionCoordinator";
|
||
|
static long sMaxWaitMillis = 1000;
|
||
|
|
||
|
private Bundle mSharedElementBundle;
|
||
|
private boolean mExitNotified;
|
||
|
private boolean mSharedElementNotified;
|
||
|
private ExitTransitionCallbacks mExitCallbacks;
|
||
|
private boolean mIsBackgroundReady;
|
||
|
private boolean mIsCanceled;
|
||
|
private Handler mHandler;
|
||
|
private ObjectAnimator mBackgroundAnimator;
|
||
|
private boolean mIsHidden;
|
||
|
private Bundle mExitSharedElementBundle;
|
||
|
private boolean mIsExitStarted;
|
||
|
private boolean mSharedElementsHidden;
|
||
|
|
||
|
public ExitTransitionCoordinator(ExitTransitionCallbacks exitCallbacks,
|
||
|
Window window, SharedElementCallback listener, ArrayList<String> names,
|
||
|
ArrayList<String> accepted, ArrayList<View> mapped, boolean isReturning) {
|
||
|
super(window, names, listener, isReturning);
|
||
|
viewsReady(mapSharedElements(accepted, mapped));
|
||
|
stripOffscreenViews();
|
||
|
mIsBackgroundReady = !isReturning;
|
||
|
mExitCallbacks = exitCallbacks;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||
|
switch (resultCode) {
|
||
|
case MSG_SET_REMOTE_RECEIVER:
|
||
|
stopCancel();
|
||
|
mResultReceiver = resultData.getParcelable(KEY_REMOTE_RECEIVER, android.os.ResultReceiver.class);
|
||
|
if (mIsCanceled) {
|
||
|
mResultReceiver.send(MSG_CANCEL, null);
|
||
|
mResultReceiver = null;
|
||
|
} else {
|
||
|
notifyComplete();
|
||
|
}
|
||
|
break;
|
||
|
case MSG_HIDE_SHARED_ELEMENTS:
|
||
|
stopCancel();
|
||
|
if (!mIsCanceled) {
|
||
|
hideSharedElements();
|
||
|
}
|
||
|
break;
|
||
|
case MSG_START_EXIT_TRANSITION:
|
||
|
mHandler.removeMessages(MSG_CANCEL);
|
||
|
startExit();
|
||
|
break;
|
||
|
case MSG_SHARED_ELEMENT_DESTINATION:
|
||
|
mExitSharedElementBundle = resultData;
|
||
|
sharedElementExitBack();
|
||
|
break;
|
||
|
case MSG_CANCEL:
|
||
|
mIsCanceled = true;
|
||
|
finish();
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void stopCancel() {
|
||
|
if (mHandler != null) {
|
||
|
mHandler.removeMessages(MSG_CANCEL);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void delayCancel() {
|
||
|
if (mHandler != null) {
|
||
|
mHandler.sendEmptyMessageDelayed(MSG_CANCEL, sMaxWaitMillis);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void resetViews() {
|
||
|
ViewGroup decorView = getDecor();
|
||
|
if (decorView != null) {
|
||
|
TransitionManager.endTransitions(decorView);
|
||
|
}
|
||
|
if (mTransitioningViews != null) {
|
||
|
showViews(mTransitioningViews, true);
|
||
|
setTransitioningViewsVisiblity(View.VISIBLE, true);
|
||
|
}
|
||
|
showViews(mSharedElements, true);
|
||
|
mIsHidden = true;
|
||
|
if (!mIsReturning && decorView != null) {
|
||
|
decorView.suppressLayout(false);
|
||
|
}
|
||
|
moveSharedElementsFromOverlay();
|
||
|
clearState();
|
||
|
}
|
||
|
|
||
|
private void sharedElementExitBack() {
|
||
|
final ViewGroup decorView = getDecor();
|
||
|
if (decorView != null) {
|
||
|
decorView.suppressLayout(true);
|
||
|
}
|
||
|
if (decorView != null && mExitSharedElementBundle != null &&
|
||
|
!mExitSharedElementBundle.isEmpty() &&
|
||
|
!mSharedElements.isEmpty() && getSharedElementTransition() != null) {
|
||
|
startTransition(new Runnable() {
|
||
|
public void run() {
|
||
|
startSharedElementExit(decorView);
|
||
|
}
|
||
|
});
|
||
|
} else {
|
||
|
sharedElementTransitionComplete();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void startSharedElementExit(final ViewGroup decorView) {
|
||
|
Transition transition = getSharedElementExitTransition();
|
||
|
transition.addListener(new TransitionListenerAdapter() {
|
||
|
@Override
|
||
|
public void onTransitionEnd(Transition transition) {
|
||
|
transition.removeListener(this);
|
||
|
if (isViewsTransitionComplete()) {
|
||
|
delayCancel();
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
final ArrayList<View> sharedElementSnapshots = createSnapshots(mExitSharedElementBundle,
|
||
|
mSharedElementNames);
|
||
|
OneShotPreDrawListener.add(decorView, () -> {
|
||
|
setSharedElementState(mExitSharedElementBundle, sharedElementSnapshots);
|
||
|
});
|
||
|
setGhostVisibility(View.INVISIBLE);
|
||
|
scheduleGhostVisibilityChange(View.INVISIBLE);
|
||
|
if (mListener != null) {
|
||
|
mListener.onSharedElementEnd(mSharedElementNames, mSharedElements,
|
||
|
sharedElementSnapshots);
|
||
|
}
|
||
|
TransitionManager.beginDelayedTransition(decorView, transition);
|
||
|
scheduleGhostVisibilityChange(View.VISIBLE);
|
||
|
setGhostVisibility(View.VISIBLE);
|
||
|
decorView.invalidate();
|
||
|
}
|
||
|
|
||
|
private void hideSharedElements() {
|
||
|
moveSharedElementsFromOverlay();
|
||
|
if (mExitCallbacks != null) {
|
||
|
mExitCallbacks.hideSharedElements();
|
||
|
}
|
||
|
if (!mIsHidden) {
|
||
|
hideViews(mSharedElements);
|
||
|
}
|
||
|
mSharedElementsHidden = true;
|
||
|
finishIfNecessary();
|
||
|
}
|
||
|
|
||
|
public void startExit() {
|
||
|
if (!mIsExitStarted) {
|
||
|
backgroundAnimatorComplete();
|
||
|
mIsExitStarted = true;
|
||
|
pauseInput();
|
||
|
ViewGroup decorView = getDecor();
|
||
|
if (decorView != null) {
|
||
|
decorView.suppressLayout(true);
|
||
|
}
|
||
|
moveSharedElementsToOverlay();
|
||
|
startTransition(this::beginTransitions);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Starts the exit animation and sends back the activity result
|
||
|
*/
|
||
|
public void startExit(Activity activity) {
|
||
|
int resultCode = activity.mResultCode;
|
||
|
Intent data = activity.mResultData;
|
||
|
if (!mIsExitStarted) {
|
||
|
mIsExitStarted = true;
|
||
|
pauseInput();
|
||
|
ViewGroup decorView = getDecor();
|
||
|
if (decorView != null) {
|
||
|
decorView.suppressLayout(true);
|
||
|
}
|
||
|
mHandler = new Handler() {
|
||
|
@Override
|
||
|
public void handleMessage(Message msg) {
|
||
|
mIsCanceled = true;
|
||
|
finish();
|
||
|
}
|
||
|
};
|
||
|
delayCancel();
|
||
|
moveSharedElementsToOverlay();
|
||
|
if (decorView != null && decorView.getBackground() == null) {
|
||
|
getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
||
|
}
|
||
|
final boolean targetsM = decorView == null || decorView.getContext()
|
||
|
.getApplicationInfo().targetSdkVersion >= VERSION_CODES.M;
|
||
|
ArrayList<String> sharedElementNames = targetsM ? mSharedElementNames :
|
||
|
mAllSharedElementNames;
|
||
|
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(activity, this,
|
||
|
sharedElementNames, resultCode, data);
|
||
|
activity.convertToTranslucent(new Activity.TranslucentConversionListener() {
|
||
|
@Override
|
||
|
public void onTranslucentConversionComplete(boolean drawComplete) {
|
||
|
if (!mIsCanceled) {
|
||
|
fadeOutBackground();
|
||
|
}
|
||
|
}
|
||
|
}, options);
|
||
|
startTransition(this::startExitTransition);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Called from {@link Activity#onStop()}
|
||
|
*/
|
||
|
public void stop(Activity activity) {
|
||
|
if (mIsReturning && mExitCallbacks != null) {
|
||
|
// Override the previous ActivityOptions. We don't want the
|
||
|
// activity to have options since we're essentially canceling the
|
||
|
// transition and finishing right now.
|
||
|
activity.convertToTranslucent(null, null);
|
||
|
finish();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void startExitTransition() {
|
||
|
Transition transition = getExitTransition();
|
||
|
ViewGroup decorView = getDecor();
|
||
|
if (transition != null && decorView != null && mTransitioningViews != null) {
|
||
|
setTransitioningViewsVisiblity(View.VISIBLE, false);
|
||
|
TransitionManager.beginDelayedTransition(decorView, transition);
|
||
|
setTransitioningViewsVisiblity(View.INVISIBLE, false);
|
||
|
decorView.invalidate();
|
||
|
} else {
|
||
|
transitionStarted();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void fadeOutBackground() {
|
||
|
if (mBackgroundAnimator == null) {
|
||
|
ViewGroup decor = getDecor();
|
||
|
Drawable background;
|
||
|
if (decor != null && (background = decor.getBackground()) != null) {
|
||
|
background = background.mutate();
|
||
|
getWindow().setBackgroundDrawable(background);
|
||
|
mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 0);
|
||
|
mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
|
||
|
@Override
|
||
|
public void onAnimationEnd(Animator animation) {
|
||
|
mBackgroundAnimator = null;
|
||
|
if (!mIsCanceled) {
|
||
|
mIsBackgroundReady = true;
|
||
|
notifyComplete();
|
||
|
}
|
||
|
backgroundAnimatorComplete();
|
||
|
}
|
||
|
});
|
||
|
mBackgroundAnimator.setDuration(getFadeDuration());
|
||
|
mBackgroundAnimator.start();
|
||
|
} else {
|
||
|
backgroundAnimatorComplete();
|
||
|
mIsBackgroundReady = true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private Transition getExitTransition() {
|
||
|
Transition viewsTransition = null;
|
||
|
if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
|
||
|
viewsTransition = configureTransition(getViewsTransition(), true);
|
||
|
removeExcludedViews(viewsTransition, mTransitioningViews);
|
||
|
if (mTransitioningViews.isEmpty()) {
|
||
|
viewsTransition = null;
|
||
|
}
|
||
|
}
|
||
|
if (viewsTransition == null) {
|
||
|
viewsTransitionComplete();
|
||
|
} else {
|
||
|
final ArrayList<View> transitioningViews = mTransitioningViews;
|
||
|
viewsTransition.addListener(new ContinueTransitionListener() {
|
||
|
@Override
|
||
|
public void onTransitionEnd(Transition transition) {
|
||
|
viewsTransitionComplete();
|
||
|
if (mIsHidden && transitioningViews != null) {
|
||
|
showViews(transitioningViews, true);
|
||
|
setTransitioningViewsVisiblity(View.VISIBLE, true);
|
||
|
}
|
||
|
if (mSharedElementBundle != null) {
|
||
|
delayCancel();
|
||
|
}
|
||
|
super.onTransitionEnd(transition);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
return viewsTransition;
|
||
|
}
|
||
|
|
||
|
private Transition getSharedElementExitTransition() {
|
||
|
Transition sharedElementTransition = null;
|
||
|
if (!mSharedElements.isEmpty()) {
|
||
|
sharedElementTransition = configureTransition(getSharedElementTransition(), false);
|
||
|
}
|
||
|
if (sharedElementTransition == null) {
|
||
|
sharedElementTransitionComplete();
|
||
|
} else {
|
||
|
sharedElementTransition.addListener(new ContinueTransitionListener() {
|
||
|
@Override
|
||
|
public void onTransitionEnd(Transition transition) {
|
||
|
sharedElementTransitionComplete();
|
||
|
if (mIsHidden) {
|
||
|
showViews(mSharedElements, true);
|
||
|
}
|
||
|
super.onTransitionEnd(transition);
|
||
|
}
|
||
|
});
|
||
|
mSharedElements.get(0).invalidate();
|
||
|
}
|
||
|
return sharedElementTransition;
|
||
|
}
|
||
|
|
||
|
private void beginTransitions() {
|
||
|
Transition sharedElementTransition = getSharedElementExitTransition();
|
||
|
Transition viewsTransition = getExitTransition();
|
||
|
|
||
|
Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
|
||
|
ViewGroup decorView = getDecor();
|
||
|
if (transition != null && decorView != null) {
|
||
|
setGhostVisibility(View.INVISIBLE);
|
||
|
scheduleGhostVisibilityChange(View.INVISIBLE);
|
||
|
if (viewsTransition != null) {
|
||
|
setTransitioningViewsVisiblity(View.VISIBLE, false);
|
||
|
}
|
||
|
TransitionManager.beginDelayedTransition(decorView, transition);
|
||
|
scheduleGhostVisibilityChange(View.VISIBLE);
|
||
|
setGhostVisibility(View.VISIBLE);
|
||
|
if (viewsTransition != null) {
|
||
|
setTransitioningViewsVisiblity(View.INVISIBLE, false);
|
||
|
}
|
||
|
decorView.invalidate();
|
||
|
} else {
|
||
|
transitionStarted();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
protected boolean isReadyToNotify() {
|
||
|
return mSharedElementBundle != null && mResultReceiver != null && mIsBackgroundReady;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void sharedElementTransitionComplete() {
|
||
|
mSharedElementBundle = mExitSharedElementBundle == null
|
||
|
? captureSharedElementState() : captureExitSharedElementsState();
|
||
|
super.sharedElementTransitionComplete();
|
||
|
}
|
||
|
|
||
|
private Bundle captureExitSharedElementsState() {
|
||
|
Bundle bundle = new Bundle();
|
||
|
RectF bounds = new RectF();
|
||
|
Matrix matrix = new Matrix();
|
||
|
for (int i = 0; i < mSharedElements.size(); i++) {
|
||
|
String name = mSharedElementNames.get(i);
|
||
|
Bundle sharedElementState = mExitSharedElementBundle.getBundle(name);
|
||
|
if (sharedElementState != null) {
|
||
|
bundle.putBundle(name, sharedElementState);
|
||
|
} else {
|
||
|
View view = mSharedElements.get(i);
|
||
|
captureSharedElementState(view, name, bundle, matrix, bounds);
|
||
|
}
|
||
|
}
|
||
|
return bundle;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void onTransitionsComplete() {
|
||
|
notifyComplete();
|
||
|
}
|
||
|
|
||
|
protected void notifyComplete() {
|
||
|
if (isReadyToNotify()) {
|
||
|
if (!mSharedElementNotified) {
|
||
|
mSharedElementNotified = true;
|
||
|
delayCancel();
|
||
|
|
||
|
if (mExitCallbacks != null && mExitCallbacks.isReturnTransitionAllowed()) {
|
||
|
mResultReceiver.send(MSG_ALLOW_RETURN_TRANSITION, null);
|
||
|
}
|
||
|
|
||
|
if (mListener == null) {
|
||
|
mResultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, mSharedElementBundle);
|
||
|
notifyExitComplete();
|
||
|
} else {
|
||
|
final ResultReceiver resultReceiver = mResultReceiver;
|
||
|
final Bundle sharedElementBundle = mSharedElementBundle;
|
||
|
mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements,
|
||
|
new OnSharedElementsReadyListener() {
|
||
|
@Override
|
||
|
public void onSharedElementsReady() {
|
||
|
resultReceiver.send(MSG_TAKE_SHARED_ELEMENTS,
|
||
|
sharedElementBundle);
|
||
|
notifyExitComplete();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
} else {
|
||
|
notifyExitComplete();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void notifyExitComplete() {
|
||
|
if (!mExitNotified && isViewsTransitionComplete()) {
|
||
|
mExitNotified = true;
|
||
|
mResultReceiver.send(MSG_EXIT_TRANSITION_COMPLETE, null);
|
||
|
mResultReceiver = null; // done talking
|
||
|
ViewGroup decorView = getDecor();
|
||
|
if (!mIsReturning && decorView != null) {
|
||
|
decorView.suppressLayout(false);
|
||
|
}
|
||
|
finishIfNecessary();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void finishIfNecessary() {
|
||
|
if (mIsReturning && mExitNotified && mExitCallbacks != null && (mSharedElements.isEmpty()
|
||
|
|| mSharedElementsHidden)) {
|
||
|
finish();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void finish() {
|
||
|
stopCancel();
|
||
|
if (mExitCallbacks != null) {
|
||
|
mExitCallbacks.onFinish();
|
||
|
mExitCallbacks = null;
|
||
|
}
|
||
|
// Clear the state so that we can't hold any references accidentally and leak memory.
|
||
|
clearState();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void clearState() {
|
||
|
mHandler = null;
|
||
|
mSharedElementBundle = null;
|
||
|
if (mBackgroundAnimator != null) {
|
||
|
mBackgroundAnimator.cancel();
|
||
|
mBackgroundAnimator = null;
|
||
|
}
|
||
|
mExitSharedElementBundle = null;
|
||
|
super.clearState();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected boolean moveSharedElementWithParent() {
|
||
|
return !mIsReturning;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected Transition getViewsTransition() {
|
||
|
if (mIsReturning) {
|
||
|
return getWindow().getReturnTransition();
|
||
|
} else {
|
||
|
return getWindow().getExitTransition();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
protected Transition getSharedElementTransition() {
|
||
|
if (mIsReturning) {
|
||
|
return getWindow().getSharedElementReturnTransition();
|
||
|
} else {
|
||
|
return getWindow().getSharedElementExitTransition();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
*/
|
||
|
public interface ExitTransitionCallbacks {
|
||
|
|
||
|
/**
|
||
|
* Returns true if reverse exit animation is supported
|
||
|
*/
|
||
|
boolean isReturnTransitionAllowed();
|
||
|
|
||
|
/**
|
||
|
* Called then the transition finishes
|
||
|
*/
|
||
|
void onFinish();
|
||
|
|
||
|
/**
|
||
|
* Optional callback when the transition is hiding elements in the source surface
|
||
|
*/
|
||
|
default void hideSharedElements() { };
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
*/
|
||
|
public static class ActivityExitTransitionCallbacks implements ExitTransitionCallbacks {
|
||
|
|
||
|
@NonNull
|
||
|
final Activity mActivity;
|
||
|
|
||
|
ActivityExitTransitionCallbacks(@NonNull Activity activity) {
|
||
|
mActivity = activity;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean isReturnTransitionAllowed() {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onFinish() {
|
||
|
mActivity.mActivityTransitionState.clear();
|
||
|
mActivity.finish();
|
||
|
mActivity.overridePendingTransition(0, 0);
|
||
|
}
|
||
|
}
|
||
|
}
|