1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.app;
17 
18 import android.animation.Animator;
19 import android.animation.AnimatorListenerAdapter;
20 import android.animation.ObjectAnimator;
21 import android.annotation.NonNull;
22 import android.app.SharedElementCallback.OnSharedElementsReadyListener;
23 import android.content.Intent;
24 import android.graphics.Color;
25 import android.graphics.Matrix;
26 import android.graphics.RectF;
27 import android.graphics.drawable.ColorDrawable;
28 import android.graphics.drawable.Drawable;
29 import android.os.Build.VERSION_CODES;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.Message;
33 import android.os.ResultReceiver;
34 import android.transition.Transition;
35 import android.transition.TransitionListenerAdapter;
36 import android.transition.TransitionManager;
37 import android.view.View;
38 import android.view.ViewGroup;
39 import android.view.Window;
40 
41 import com.android.internal.view.OneShotPreDrawListener;
42 
43 import java.util.ArrayList;
44 
45 /**
46  * This ActivityTransitionCoordinator is created in ActivityOptions#makeSceneTransitionAnimation
47  * to govern the exit of the Scene and the shared elements when calling an Activity as well as
48  * the reentry of the Scene when coming back from the called Activity.
49  *
50  * @hide
51  */
52 public class ExitTransitionCoordinator extends ActivityTransitionCoordinator {
53     private static final String TAG = "ExitTransitionCoordinator";
54     static long sMaxWaitMillis = 1000;
55 
56     private Bundle mSharedElementBundle;
57     private boolean mExitNotified;
58     private boolean mSharedElementNotified;
59     private ExitTransitionCallbacks mExitCallbacks;
60     private boolean mIsBackgroundReady;
61     private boolean mIsCanceled;
62     private Handler mHandler;
63     private ObjectAnimator mBackgroundAnimator;
64     private boolean mIsHidden;
65     private Bundle mExitSharedElementBundle;
66     private boolean mIsExitStarted;
67     private boolean mSharedElementsHidden;
68 
ExitTransitionCoordinator(ExitTransitionCallbacks exitCallbacks, Window window, SharedElementCallback listener, ArrayList<String> names, ArrayList<String> accepted, ArrayList<View> mapped, boolean isReturning)69     public ExitTransitionCoordinator(ExitTransitionCallbacks exitCallbacks,
70             Window window, SharedElementCallback listener, ArrayList<String> names,
71             ArrayList<String> accepted, ArrayList<View> mapped, boolean isReturning) {
72         super(window, names, listener, isReturning);
73         viewsReady(mapSharedElements(accepted, mapped));
74         stripOffscreenViews();
75         mIsBackgroundReady = !isReturning;
76         mExitCallbacks = exitCallbacks;
77     }
78 
79     @Override
onReceiveResult(int resultCode, Bundle resultData)80     protected void onReceiveResult(int resultCode, Bundle resultData) {
81         switch (resultCode) {
82             case MSG_SET_REMOTE_RECEIVER:
83                 stopCancel();
84                 mResultReceiver = resultData.getParcelable(KEY_REMOTE_RECEIVER, android.os.ResultReceiver.class);
85                 if (mIsCanceled) {
86                     mResultReceiver.send(MSG_CANCEL, null);
87                     mResultReceiver = null;
88                 } else {
89                     notifyComplete();
90                 }
91                 break;
92             case MSG_HIDE_SHARED_ELEMENTS:
93                 stopCancel();
94                 if (!mIsCanceled) {
95                     hideSharedElements();
96                 }
97                 break;
98             case MSG_START_EXIT_TRANSITION:
99                 mHandler.removeMessages(MSG_CANCEL);
100                 startExit();
101                 break;
102             case MSG_SHARED_ELEMENT_DESTINATION:
103                 mExitSharedElementBundle = resultData;
104                 sharedElementExitBack();
105                 break;
106             case MSG_CANCEL:
107                 mIsCanceled = true;
108                 finish();
109                 break;
110         }
111     }
112 
stopCancel()113     private void stopCancel() {
114         if (mHandler != null) {
115             mHandler.removeMessages(MSG_CANCEL);
116         }
117     }
118 
delayCancel()119     private void delayCancel() {
120         if (mHandler != null) {
121             mHandler.sendEmptyMessageDelayed(MSG_CANCEL, sMaxWaitMillis);
122         }
123     }
124 
resetViews()125     public void resetViews() {
126         ViewGroup decorView = getDecor();
127         if (decorView != null) {
128             TransitionManager.endTransitions(decorView);
129         }
130         if (mTransitioningViews != null) {
131             showViews(mTransitioningViews, true);
132             setTransitioningViewsVisiblity(View.VISIBLE, true);
133         }
134         showViews(mSharedElements, true);
135         mIsHidden = true;
136         if (!mIsReturning && decorView != null) {
137             decorView.suppressLayout(false);
138         }
139         moveSharedElementsFromOverlay();
140         clearState();
141     }
142 
sharedElementExitBack()143     private void sharedElementExitBack() {
144         final ViewGroup decorView = getDecor();
145         if (decorView != null) {
146             decorView.suppressLayout(true);
147         }
148         if (decorView != null && mExitSharedElementBundle != null &&
149                 !mExitSharedElementBundle.isEmpty() &&
150                 !mSharedElements.isEmpty() && getSharedElementTransition() != null) {
151             startTransition(new Runnable() {
152                 public void run() {
153                     startSharedElementExit(decorView);
154                 }
155             });
156         } else {
157             sharedElementTransitionComplete();
158         }
159     }
160 
startSharedElementExit(final ViewGroup decorView)161     private void startSharedElementExit(final ViewGroup decorView) {
162         Transition transition = getSharedElementExitTransition();
163         transition.addListener(new TransitionListenerAdapter() {
164             @Override
165             public void onTransitionEnd(Transition transition) {
166                 transition.removeListener(this);
167                 if (isViewsTransitionComplete()) {
168                     delayCancel();
169                 }
170             }
171         });
172         final ArrayList<View> sharedElementSnapshots = createSnapshots(mExitSharedElementBundle,
173                 mSharedElementNames);
174         OneShotPreDrawListener.add(decorView, () -> {
175             setSharedElementState(mExitSharedElementBundle, sharedElementSnapshots);
176         });
177         setGhostVisibility(View.INVISIBLE);
178         scheduleGhostVisibilityChange(View.INVISIBLE);
179         if (mListener != null) {
180             mListener.onSharedElementEnd(mSharedElementNames, mSharedElements,
181                     sharedElementSnapshots);
182         }
183         TransitionManager.beginDelayedTransition(decorView, transition);
184         scheduleGhostVisibilityChange(View.VISIBLE);
185         setGhostVisibility(View.VISIBLE);
186         decorView.invalidate();
187     }
188 
hideSharedElements()189     private void hideSharedElements() {
190         moveSharedElementsFromOverlay();
191         if (mExitCallbacks != null) {
192             mExitCallbacks.hideSharedElements();
193         }
194         if (!mIsHidden) {
195             hideViews(mSharedElements);
196         }
197         mSharedElementsHidden = true;
198         finishIfNecessary();
199     }
200 
startExit()201     public void startExit() {
202         if (!mIsExitStarted) {
203             backgroundAnimatorComplete();
204             mIsExitStarted = true;
205             pauseInput();
206             ViewGroup decorView = getDecor();
207             if (decorView != null) {
208                 decorView.suppressLayout(true);
209             }
210             moveSharedElementsToOverlay();
211             startTransition(this::beginTransitions);
212         }
213     }
214 
215     /**
216      * Starts the exit animation and sends back the activity result
217      */
startExit(Activity activity)218     public void startExit(Activity activity) {
219         int resultCode = activity.mResultCode;
220         Intent data = activity.mResultData;
221         if (!mIsExitStarted) {
222             mIsExitStarted = true;
223             pauseInput();
224             ViewGroup decorView = getDecor();
225             if (decorView != null) {
226                 decorView.suppressLayout(true);
227             }
228             mHandler = new Handler() {
229                 @Override
230                 public void handleMessage(Message msg) {
231                     mIsCanceled = true;
232                     finish();
233                 }
234             };
235             delayCancel();
236             moveSharedElementsToOverlay();
237             if (decorView != null && decorView.getBackground() == null) {
238                 getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
239             }
240             final boolean targetsM = decorView == null || decorView.getContext()
241                     .getApplicationInfo().targetSdkVersion >= VERSION_CODES.M;
242             ArrayList<String> sharedElementNames = targetsM ? mSharedElementNames :
243                     mAllSharedElementNames;
244             ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(activity, this,
245                     sharedElementNames, resultCode, data);
246             activity.convertToTranslucent(new Activity.TranslucentConversionListener() {
247                 @Override
248                 public void onTranslucentConversionComplete(boolean drawComplete) {
249                     if (!mIsCanceled) {
250                         fadeOutBackground();
251                     }
252                 }
253             }, options);
254             startTransition(this::startExitTransition);
255         }
256     }
257 
258     /**
259      * Called from {@link Activity#onStop()}
260      */
stop(Activity activity)261     public void stop(Activity activity) {
262         if (mIsReturning && mExitCallbacks != null) {
263             // Override the previous ActivityOptions. We don't want the
264             // activity to have options since we're essentially canceling the
265             // transition and finishing right now.
266             activity.convertToTranslucent(null, null);
267             finish();
268         }
269     }
270 
startExitTransition()271     private void startExitTransition() {
272         Transition transition = getExitTransition();
273         ViewGroup decorView = getDecor();
274         if (transition != null && decorView != null && mTransitioningViews != null) {
275             setTransitioningViewsVisiblity(View.VISIBLE, false);
276             TransitionManager.beginDelayedTransition(decorView, transition);
277             setTransitioningViewsVisiblity(View.INVISIBLE, false);
278             decorView.invalidate();
279         } else {
280             transitionStarted();
281         }
282     }
283 
fadeOutBackground()284     private void fadeOutBackground() {
285         if (mBackgroundAnimator == null) {
286             ViewGroup decor = getDecor();
287             Drawable background;
288             if (decor != null && (background = decor.getBackground()) != null) {
289                 background = background.mutate();
290                 getWindow().setBackgroundDrawable(background);
291                 mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 0);
292                 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
293                     @Override
294                     public void onAnimationEnd(Animator animation) {
295                         mBackgroundAnimator = null;
296                         if (!mIsCanceled) {
297                             mIsBackgroundReady = true;
298                             notifyComplete();
299                         }
300                         backgroundAnimatorComplete();
301                     }
302                 });
303                 mBackgroundAnimator.setDuration(getFadeDuration());
304                 mBackgroundAnimator.start();
305             } else {
306                 backgroundAnimatorComplete();
307                 mIsBackgroundReady = true;
308             }
309         }
310     }
311 
getExitTransition()312     private Transition getExitTransition() {
313         Transition viewsTransition = null;
314         if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
315             viewsTransition = configureTransition(getViewsTransition(), true);
316             removeExcludedViews(viewsTransition, mTransitioningViews);
317             if (mTransitioningViews.isEmpty()) {
318                 viewsTransition = null;
319             }
320         }
321         if (viewsTransition == null) {
322             viewsTransitionComplete();
323         } else {
324             final ArrayList<View> transitioningViews = mTransitioningViews;
325             viewsTransition.addListener(new ContinueTransitionListener() {
326                 @Override
327                 public void onTransitionEnd(Transition transition) {
328                     viewsTransitionComplete();
329                     if (mIsHidden && transitioningViews != null) {
330                         showViews(transitioningViews, true);
331                         setTransitioningViewsVisiblity(View.VISIBLE, true);
332                     }
333                     if (mSharedElementBundle != null) {
334                         delayCancel();
335                     }
336                     super.onTransitionEnd(transition);
337                 }
338             });
339         }
340         return viewsTransition;
341     }
342 
getSharedElementExitTransition()343     private Transition getSharedElementExitTransition() {
344         Transition sharedElementTransition = null;
345         if (!mSharedElements.isEmpty()) {
346             sharedElementTransition = configureTransition(getSharedElementTransition(), false);
347         }
348         if (sharedElementTransition == null) {
349             sharedElementTransitionComplete();
350         } else {
351             sharedElementTransition.addListener(new ContinueTransitionListener() {
352                 @Override
353                 public void onTransitionEnd(Transition transition) {
354                     sharedElementTransitionComplete();
355                     if (mIsHidden) {
356                         showViews(mSharedElements, true);
357                     }
358                     super.onTransitionEnd(transition);
359                 }
360             });
361             mSharedElements.get(0).invalidate();
362         }
363         return sharedElementTransition;
364     }
365 
beginTransitions()366     private void beginTransitions() {
367         Transition sharedElementTransition = getSharedElementExitTransition();
368         Transition viewsTransition = getExitTransition();
369 
370         Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
371         ViewGroup decorView = getDecor();
372         if (transition != null && decorView != null) {
373             setGhostVisibility(View.INVISIBLE);
374             scheduleGhostVisibilityChange(View.INVISIBLE);
375             if (viewsTransition != null) {
376                 setTransitioningViewsVisiblity(View.VISIBLE, false);
377             }
378             TransitionManager.beginDelayedTransition(decorView, transition);
379             scheduleGhostVisibilityChange(View.VISIBLE);
380             setGhostVisibility(View.VISIBLE);
381             if (viewsTransition != null) {
382                 setTransitioningViewsVisiblity(View.INVISIBLE, false);
383             }
384             decorView.invalidate();
385         } else {
386             transitionStarted();
387         }
388     }
389 
isReadyToNotify()390     protected boolean isReadyToNotify() {
391         return mSharedElementBundle != null && mResultReceiver != null && mIsBackgroundReady;
392     }
393 
394     @Override
sharedElementTransitionComplete()395     protected void sharedElementTransitionComplete() {
396         mSharedElementBundle = mExitSharedElementBundle == null
397                 ? captureSharedElementState() : captureExitSharedElementsState();
398         super.sharedElementTransitionComplete();
399     }
400 
captureExitSharedElementsState()401     private Bundle captureExitSharedElementsState() {
402         Bundle bundle = new Bundle();
403         RectF bounds = new RectF();
404         Matrix matrix = new Matrix();
405         for (int i = 0; i < mSharedElements.size(); i++) {
406             String name = mSharedElementNames.get(i);
407             Bundle sharedElementState = mExitSharedElementBundle.getBundle(name);
408             if (sharedElementState != null) {
409                 bundle.putBundle(name, sharedElementState);
410             } else {
411                 View view = mSharedElements.get(i);
412                 captureSharedElementState(view, name, bundle, matrix, bounds);
413             }
414         }
415         return bundle;
416     }
417 
418     @Override
onTransitionsComplete()419     protected void onTransitionsComplete() {
420         notifyComplete();
421     }
422 
notifyComplete()423     protected void notifyComplete() {
424         if (isReadyToNotify()) {
425             if (!mSharedElementNotified) {
426                 mSharedElementNotified = true;
427                 delayCancel();
428 
429                 if (mExitCallbacks != null && mExitCallbacks.isReturnTransitionAllowed()) {
430                     mResultReceiver.send(MSG_ALLOW_RETURN_TRANSITION, null);
431                 }
432 
433                 if (mListener == null) {
434                     mResultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, mSharedElementBundle);
435                     notifyExitComplete();
436                 } else {
437                     final ResultReceiver resultReceiver = mResultReceiver;
438                     final Bundle sharedElementBundle = mSharedElementBundle;
439                     mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements,
440                             new OnSharedElementsReadyListener() {
441                                 @Override
442                                 public void onSharedElementsReady() {
443                                     resultReceiver.send(MSG_TAKE_SHARED_ELEMENTS,
444                                             sharedElementBundle);
445                                     notifyExitComplete();
446                                 }
447                             });
448                 }
449             } else {
450                 notifyExitComplete();
451             }
452         }
453     }
454 
notifyExitComplete()455     private void notifyExitComplete() {
456         if (!mExitNotified && isViewsTransitionComplete()) {
457             mExitNotified = true;
458             mResultReceiver.send(MSG_EXIT_TRANSITION_COMPLETE, null);
459             mResultReceiver = null; // done talking
460             ViewGroup decorView = getDecor();
461             if (!mIsReturning && decorView != null) {
462                 decorView.suppressLayout(false);
463             }
464             finishIfNecessary();
465         }
466     }
467 
finishIfNecessary()468     private void finishIfNecessary() {
469         if (mIsReturning && mExitNotified && mExitCallbacks != null && (mSharedElements.isEmpty()
470                 || mSharedElementsHidden)) {
471             finish();
472         }
473     }
474 
finish()475     private void finish() {
476         stopCancel();
477         if (mExitCallbacks != null) {
478             mExitCallbacks.onFinish();
479             mExitCallbacks = null;
480         }
481         // Clear the state so that we can't hold any references accidentally and leak memory.
482         clearState();
483     }
484 
485     @Override
clearState()486     protected void clearState() {
487         mHandler = null;
488         mSharedElementBundle = null;
489         if (mBackgroundAnimator != null) {
490             mBackgroundAnimator.cancel();
491             mBackgroundAnimator = null;
492         }
493         mExitSharedElementBundle = null;
494         super.clearState();
495     }
496 
497     @Override
moveSharedElementWithParent()498     protected boolean moveSharedElementWithParent() {
499         return !mIsReturning;
500     }
501 
502     @Override
getViewsTransition()503     protected Transition getViewsTransition() {
504         if (mIsReturning) {
505             return getWindow().getReturnTransition();
506         } else {
507             return getWindow().getExitTransition();
508         }
509     }
510 
getSharedElementTransition()511     protected Transition getSharedElementTransition() {
512         if (mIsReturning) {
513             return getWindow().getSharedElementReturnTransition();
514         } else {
515             return getWindow().getSharedElementExitTransition();
516         }
517     }
518 
519     /**
520      * @hide
521      */
522     public interface ExitTransitionCallbacks {
523 
524         /**
525          * Returns true if reverse exit animation is supported
526          */
isReturnTransitionAllowed()527         boolean isReturnTransitionAllowed();
528 
529         /**
530          * Called then the transition finishes
531          */
onFinish()532         void onFinish();
533 
534         /**
535          * Optional callback when the transition is hiding elements in the source surface
536          */
hideSharedElements()537         default void hideSharedElements() { };
538     }
539 
540     /**
541      * @hide
542      */
543     public static class ActivityExitTransitionCallbacks implements ExitTransitionCallbacks {
544 
545         @NonNull
546         final Activity mActivity;
547 
ActivityExitTransitionCallbacks(@onNull Activity activity)548         ActivityExitTransitionCallbacks(@NonNull Activity activity) {
549             mActivity = activity;
550         }
551 
552         @Override
isReturnTransitionAllowed()553         public boolean isReturnTransitionAllowed() {
554             return true;
555         }
556 
557         @Override
onFinish()558         public void onFinish() {
559             mActivity.mActivityTransitionState.clear();
560             mActivity.finish();
561             mActivity.overridePendingTransition(0, 0);
562         }
563     }
564 }
565