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