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.graphics.drawable.Drawable;
23 import android.os.Bundle;
24 import android.os.ResultReceiver;
25 import android.text.TextUtils;
26 import android.transition.Transition;
27 import android.transition.TransitionManager;
28 import android.util.ArrayMap;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.ViewGroupOverlay;
32 import android.view.ViewTreeObserver;
33 import android.view.Window;
34 
35 import java.util.ArrayList;
36 
37 /**
38  * This ActivityTransitionCoordinator is created by the Activity to manage
39  * the enter scene and shared element transfer into the Scene, either during
40  * launch of an Activity or returning from a launched Activity.
41  */
42 class EnterTransitionCoordinator extends ActivityTransitionCoordinator {
43     private static final String TAG = "EnterTransitionCoordinator";
44 
45     private static final int MIN_ANIMATION_FRAMES = 2;
46 
47     private boolean mSharedElementTransitionStarted;
48     private Activity mActivity;
49     private boolean mHasStopped;
50     private boolean mIsCanceled;
51     private ObjectAnimator mBackgroundAnimator;
52     private boolean mIsExitTransitionComplete;
53     private boolean mIsReadyForTransition;
54     private Bundle mSharedElementsBundle;
55     private boolean mWasOpaque;
56     private boolean mAreViewsReady;
57     private boolean mIsViewsTransitionStarted;
58     private Transition mEnterViewsTransition;
59 
EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver, ArrayList<String> sharedElementNames, boolean isReturning)60     public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver,
61             ArrayList<String> sharedElementNames, boolean isReturning) {
62         super(activity.getWindow(), sharedElementNames,
63                 getListener(activity, isReturning), isReturning);
64         mActivity = activity;
65         setResultReceiver(resultReceiver);
66         prepareEnter();
67         Bundle resultReceiverBundle = new Bundle();
68         resultReceiverBundle.putParcelable(KEY_REMOTE_RECEIVER, this);
69         mResultReceiver.send(MSG_SET_REMOTE_RECEIVER, resultReceiverBundle);
70         final View decorView = getDecor();
71         if (decorView != null) {
72             decorView.getViewTreeObserver().addOnPreDrawListener(
73                     new ViewTreeObserver.OnPreDrawListener() {
74                         @Override
75                         public boolean onPreDraw() {
76                             if (mIsReadyForTransition) {
77                                 decorView.getViewTreeObserver().removeOnPreDrawListener(this);
78                             }
79                             return mIsReadyForTransition;
80                         }
81                     });
82         }
83     }
84 
viewInstancesReady(ArrayList<String> accepted, ArrayList<String> localNames, ArrayList<View> localViews)85     public void viewInstancesReady(ArrayList<String> accepted, ArrayList<String> localNames,
86             ArrayList<View> localViews) {
87         boolean remap = false;
88         for (int i = 0; i < localViews.size(); i++) {
89             View view = localViews.get(i);
90             if (!TextUtils.equals(view.getTransitionName(), localNames.get(i))
91                     || !view.isAttachedToWindow()) {
92                 remap = true;
93                 break;
94             }
95         }
96         if (remap) {
97             triggerViewsReady(mapNamedElements(accepted, localNames));
98         } else {
99             triggerViewsReady(mapSharedElements(accepted, localViews));
100         }
101     }
102 
namedViewsReady(ArrayList<String> accepted, ArrayList<String> localNames)103     public void namedViewsReady(ArrayList<String> accepted, ArrayList<String> localNames) {
104         triggerViewsReady(mapNamedElements(accepted, localNames));
105     }
106 
getEnterViewsTransition()107     public Transition getEnterViewsTransition() {
108         return mEnterViewsTransition;
109     }
110 
111     @Override
viewsReady(ArrayMap<String, View> sharedElements)112     protected void viewsReady(ArrayMap<String, View> sharedElements) {
113         super.viewsReady(sharedElements);
114         mIsReadyForTransition = true;
115         hideViews(mSharedElements);
116         if (getViewsTransition() != null && mTransitioningViews != null) {
117             hideViews(mTransitioningViews);
118         }
119         if (mIsReturning) {
120             sendSharedElementDestination();
121         } else {
122             moveSharedElementsToOverlay();
123         }
124         if (mSharedElementsBundle != null) {
125             onTakeSharedElements();
126         }
127     }
128 
triggerViewsReady(final ArrayMap<String, View> sharedElements)129     private void triggerViewsReady(final ArrayMap<String, View> sharedElements) {
130         if (mAreViewsReady) {
131             return;
132         }
133         mAreViewsReady = true;
134         final ViewGroup decor = getDecor();
135         // Ensure the views have been laid out before capturing the views -- we need the epicenter.
136         if (decor == null || (decor.isAttachedToWindow() &&
137                 (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()))) {
138             viewsReady(sharedElements);
139         } else {
140             decor.getViewTreeObserver()
141                     .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
142                         @Override
143                         public boolean onPreDraw() {
144                             decor.getViewTreeObserver().removeOnPreDrawListener(this);
145                             viewsReady(sharedElements);
146                             return true;
147                         }
148                     });
149         }
150     }
151 
mapNamedElements(ArrayList<String> accepted, ArrayList<String> localNames)152     private ArrayMap<String, View> mapNamedElements(ArrayList<String> accepted,
153             ArrayList<String> localNames) {
154         ArrayMap<String, View> sharedElements = new ArrayMap<String, View>();
155         ViewGroup decorView = getDecor();
156         if (decorView != null) {
157             decorView.findNamedViews(sharedElements);
158         }
159         if (accepted != null) {
160             for (int i = 0; i < localNames.size(); i++) {
161                 String localName = localNames.get(i);
162                 String acceptedName = accepted.get(i);
163                 if (localName != null && !localName.equals(acceptedName)) {
164                     View view = sharedElements.remove(localName);
165                     if (view != null) {
166                         sharedElements.put(acceptedName, view);
167                     }
168                 }
169             }
170         }
171         return sharedElements;
172     }
173 
sendSharedElementDestination()174     private void sendSharedElementDestination() {
175         boolean allReady;
176         final View decorView = getDecor();
177         if (allowOverlappingTransitions() && getEnterViewsTransition() != null) {
178             allReady = false;
179         } else if (decorView == null) {
180             allReady = true;
181         } else {
182             allReady = !decorView.isLayoutRequested();
183             if (allReady) {
184                 for (int i = 0; i < mSharedElements.size(); i++) {
185                     if (mSharedElements.get(i).isLayoutRequested()) {
186                         allReady = false;
187                         break;
188                     }
189                 }
190             }
191         }
192         if (allReady) {
193             Bundle state = captureSharedElementState();
194             moveSharedElementsToOverlay();
195             mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state);
196         } else if (decorView != null) {
197             decorView.getViewTreeObserver()
198                     .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
199                         @Override
200                         public boolean onPreDraw() {
201                             decorView.getViewTreeObserver().removeOnPreDrawListener(this);
202                             if (mResultReceiver != null) {
203                                 Bundle state = captureSharedElementState();
204                                 moveSharedElementsToOverlay();
205                                 mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state);
206                             }
207                             return true;
208                         }
209                     });
210         }
211         if (allowOverlappingTransitions()) {
212             startEnterTransitionOnly();
213         }
214     }
215 
getListener(Activity activity, boolean isReturning)216     private static SharedElementCallback getListener(Activity activity, boolean isReturning) {
217         return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener;
218     }
219 
220     @Override
onReceiveResult(int resultCode, Bundle resultData)221     protected void onReceiveResult(int resultCode, Bundle resultData) {
222         switch (resultCode) {
223             case MSG_TAKE_SHARED_ELEMENTS:
224                 if (!mIsCanceled) {
225                     mSharedElementsBundle = resultData;
226                     onTakeSharedElements();
227                 }
228                 break;
229             case MSG_EXIT_TRANSITION_COMPLETE:
230                 if (!mIsCanceled) {
231                     mIsExitTransitionComplete = true;
232                     if (mSharedElementTransitionStarted) {
233                         onRemoteExitTransitionComplete();
234                     }
235                 }
236                 break;
237             case MSG_CANCEL:
238                 cancel();
239                 break;
240         }
241     }
242 
cancel()243     private void cancel() {
244         if (!mIsCanceled) {
245             mIsCanceled = true;
246             if (getViewsTransition() == null || mIsViewsTransitionStarted) {
247                 showViews(mSharedElements, true);
248             } else if (mTransitioningViews != null) {
249                 mTransitioningViews.addAll(mSharedElements);
250             }
251             mSharedElementNames.clear();
252             mSharedElements.clear();
253             mAllSharedElementNames.clear();
254             startSharedElementTransition(null);
255             onRemoteExitTransitionComplete();
256         }
257     }
258 
isReturning()259     public boolean isReturning() {
260         return mIsReturning;
261     }
262 
prepareEnter()263     protected void prepareEnter() {
264         ViewGroup decorView = getDecor();
265         if (mActivity == null || decorView == null) {
266             return;
267         }
268         mActivity.overridePendingTransition(0, 0);
269         if (!mIsReturning) {
270             mWasOpaque = mActivity.convertToTranslucent(null, null);
271             Drawable background = decorView.getBackground();
272             if (background != null) {
273                 getWindow().setBackgroundDrawable(null);
274                 background = background.mutate();
275                 background.setAlpha(0);
276                 getWindow().setBackgroundDrawable(background);
277             }
278         } else {
279             mActivity = null; // all done with it now.
280         }
281     }
282 
283     @Override
getViewsTransition()284     protected Transition getViewsTransition() {
285         Window window = getWindow();
286         if (window == null) {
287             return null;
288         }
289         if (mIsReturning) {
290             return window.getReenterTransition();
291         } else {
292             return window.getEnterTransition();
293         }
294     }
295 
getSharedElementTransition()296     protected Transition getSharedElementTransition() {
297         Window window = getWindow();
298         if (window == null) {
299             return null;
300         }
301         if (mIsReturning) {
302             return window.getSharedElementReenterTransition();
303         } else {
304             return window.getSharedElementEnterTransition();
305         }
306     }
307 
startSharedElementTransition(Bundle sharedElementState)308     private void startSharedElementTransition(Bundle sharedElementState) {
309         ViewGroup decorView = getDecor();
310         if (decorView == null) {
311             return;
312         }
313         // Remove rejected shared elements
314         ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames);
315         rejectedNames.removeAll(mSharedElementNames);
316         ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames);
317         if (mListener != null) {
318             mListener.onRejectSharedElements(rejectedSnapshots);
319         }
320         removeNullViews(rejectedSnapshots);
321         startRejectedAnimations(rejectedSnapshots);
322 
323         // Now start shared element transition
324         ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState,
325                 mSharedElementNames);
326         showViews(mSharedElements, true);
327         scheduleSetSharedElementEnd(sharedElementSnapshots);
328         ArrayList<SharedElementOriginalState> originalImageViewState =
329                 setSharedElementState(sharedElementState, sharedElementSnapshots);
330         requestLayoutForSharedElements();
331 
332         boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning;
333         boolean startSharedElementTransition = true;
334         setGhostVisibility(View.INVISIBLE);
335         scheduleGhostVisibilityChange(View.INVISIBLE);
336         pauseInput();
337         Transition transition = beginTransition(decorView, startEnterTransition,
338                 startSharedElementTransition);
339         scheduleGhostVisibilityChange(View.VISIBLE);
340         setGhostVisibility(View.VISIBLE);
341 
342         if (startEnterTransition) {
343             startEnterTransition(transition);
344         }
345 
346         setOriginalSharedElementState(mSharedElements, originalImageViewState);
347 
348         if (mResultReceiver != null) {
349             // We can't trust that the view will disappear on the same frame that the shared
350             // element appears here. Assure that we get at least 2 frames for double-buffering.
351             decorView.postOnAnimation(new Runnable() {
352                 int mAnimations;
353 
354                 @Override
355                 public void run() {
356                     if (mAnimations++ < MIN_ANIMATION_FRAMES) {
357                         View decorView = getDecor();
358                         if (decorView != null) {
359                             decorView.postOnAnimation(this);
360                         }
361                     } else if (mResultReceiver != null) {
362                         mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null);
363                         mResultReceiver = null; // all done sending messages.
364                     }
365                 }
366             });
367         }
368     }
369 
removeNullViews(ArrayList<View> views)370     private static void removeNullViews(ArrayList<View> views) {
371         if (views != null) {
372             for (int i = views.size() - 1; i >= 0; i--) {
373                 if (views.get(i) == null) {
374                     views.remove(i);
375                 }
376             }
377         }
378     }
379 
onTakeSharedElements()380     private void onTakeSharedElements() {
381         if (!mIsReadyForTransition || mSharedElementsBundle == null) {
382             return;
383         }
384         final Bundle sharedElementState = mSharedElementsBundle;
385         mSharedElementsBundle = null;
386         OnSharedElementsReadyListener listener = new OnSharedElementsReadyListener() {
387             @Override
388             public void onSharedElementsReady() {
389                 final View decorView = getDecor();
390                 if (decorView != null) {
391                     decorView.getViewTreeObserver()
392                             .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
393                                 @Override
394                                 public boolean onPreDraw() {
395                                     decorView.getViewTreeObserver().removeOnPreDrawListener(this);
396                                     startTransition(new Runnable() {
397                                         @Override
398                                         public void run() {
399                                             startSharedElementTransition(sharedElementState);
400                                         }
401                                     });
402                                     return false;
403                                 }
404                             });
405                     decorView.invalidate();
406                 }
407             }
408         };
409         if (mListener == null) {
410             listener.onSharedElementsReady();
411         } else {
412             mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements, listener);
413         }
414     }
415 
requestLayoutForSharedElements()416     private void requestLayoutForSharedElements() {
417         int numSharedElements = mSharedElements.size();
418         for (int i = 0; i < numSharedElements; i++) {
419             mSharedElements.get(i).requestLayout();
420         }
421     }
422 
beginTransition(ViewGroup decorView, boolean startEnterTransition, boolean startSharedElementTransition)423     private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition,
424             boolean startSharedElementTransition) {
425         Transition sharedElementTransition = null;
426         if (startSharedElementTransition) {
427             if (!mSharedElementNames.isEmpty()) {
428                 sharedElementTransition = configureTransition(getSharedElementTransition(), false);
429             }
430             if (sharedElementTransition == null) {
431                 sharedElementTransitionStarted();
432                 sharedElementTransitionComplete();
433             } else {
434                 sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() {
435                     @Override
436                     public void onTransitionStart(Transition transition) {
437                         sharedElementTransitionStarted();
438                     }
439 
440                     @Override
441                     public void onTransitionEnd(Transition transition) {
442                         transition.removeListener(this);
443                         sharedElementTransitionComplete();
444                     }
445                 });
446             }
447         }
448         Transition viewsTransition = null;
449         if (startEnterTransition) {
450             mIsViewsTransitionStarted = true;
451             if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
452                 viewsTransition = configureTransition(getViewsTransition(), true);
453                 if (viewsTransition != null && !mIsReturning) {
454                     stripOffscreenViews();
455                 }
456             }
457             if (viewsTransition == null) {
458                 viewsTransitionComplete();
459             } else {
460                 viewsTransition.forceVisibility(View.INVISIBLE, true);
461                 final ArrayList<View> transitioningViews = mTransitioningViews;
462                 viewsTransition.addListener(new ContinueTransitionListener() {
463                     @Override
464                     public void onTransitionStart(Transition transition) {
465                         mEnterViewsTransition = transition;
466                         if (transitioningViews != null) {
467                             showViews(transitioningViews, false);
468                         }
469                         super.onTransitionStart(transition);
470                     }
471 
472                     @Override
473                     public void onTransitionEnd(Transition transition) {
474                         mEnterViewsTransition = null;
475                         transition.removeListener(this);
476                         viewsTransitionComplete();
477                         super.onTransitionEnd(transition);
478                     }
479                 });
480             }
481         }
482 
483         Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
484         if (transition != null) {
485             transition.addListener(new ContinueTransitionListener());
486             TransitionManager.beginDelayedTransition(decorView, transition);
487             if (startSharedElementTransition && !mSharedElementNames.isEmpty()) {
488                 mSharedElements.get(0).invalidate();
489             } else if (startEnterTransition && mTransitioningViews != null &&
490                     !mTransitioningViews.isEmpty()) {
491                 mTransitioningViews.get(0).invalidate();
492             }
493         } else {
494             transitionStarted();
495         }
496         return transition;
497     }
498 
499     @Override
onTransitionsComplete()500     protected void onTransitionsComplete() {
501         moveSharedElementsFromOverlay();
502     }
503 
sharedElementTransitionStarted()504     private void sharedElementTransitionStarted() {
505         mSharedElementTransitionStarted = true;
506         if (mIsExitTransitionComplete) {
507             send(MSG_EXIT_TRANSITION_COMPLETE, null);
508         }
509     }
510 
startEnterTransition(Transition transition)511     private void startEnterTransition(Transition transition) {
512         ViewGroup decorView = getDecor();
513         if (!mIsReturning && decorView != null) {
514             Drawable background = decorView.getBackground();
515             if (background != null) {
516                 background = background.mutate();
517                 getWindow().setBackgroundDrawable(background);
518                 mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255);
519                 mBackgroundAnimator.setDuration(getFadeDuration());
520                 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
521                     @Override
522                     public void onAnimationEnd(Animator animation) {
523                         makeOpaque();
524                     }
525                 });
526                 mBackgroundAnimator.start();
527             } else if (transition != null) {
528                 transition.addListener(new Transition.TransitionListenerAdapter() {
529                     @Override
530                     public void onTransitionEnd(Transition transition) {
531                         transition.removeListener(this);
532                         makeOpaque();
533                     }
534                 });
535             } else {
536                 makeOpaque();
537             }
538         }
539     }
540 
stop()541     public void stop() {
542         // Restore the background to its previous state since the
543         // Activity is stopping.
544         if (mBackgroundAnimator != null) {
545             mBackgroundAnimator.end();
546             mBackgroundAnimator = null;
547         } else if (mWasOpaque) {
548             ViewGroup decorView = getDecor();
549             if (decorView != null) {
550                 Drawable drawable = decorView.getBackground();
551                 if (drawable != null) {
552                     drawable.setAlpha(1);
553                 }
554             }
555         }
556         makeOpaque();
557         mIsCanceled = true;
558         mResultReceiver = null;
559         mActivity = null;
560         moveSharedElementsFromOverlay();
561         if (mTransitioningViews != null) {
562             showViews(mTransitioningViews, true);
563         }
564         showViews(mSharedElements, true);
565         clearState();
566     }
567 
568     /**
569      * Cancels the enter transition.
570      * @return True if the enter transition is still pending capturing the target state. If so,
571      * any transition started on the decor will do nothing.
572      */
cancelEnter()573     public boolean cancelEnter() {
574         setGhostVisibility(View.INVISIBLE);
575         mHasStopped = true;
576         mIsCanceled = true;
577         mResultReceiver = null;
578         if (mBackgroundAnimator != null) {
579             mBackgroundAnimator.cancel();
580             mBackgroundAnimator = null;
581         }
582         mActivity = null;
583         clearState();
584         return super.cancelPendingTransitions();
585     }
586 
makeOpaque()587     private void makeOpaque() {
588         if (!mHasStopped && mActivity != null) {
589             if (mWasOpaque) {
590                 mActivity.convertFromTranslucent();
591             }
592             mActivity = null;
593         }
594     }
595 
allowOverlappingTransitions()596     private boolean allowOverlappingTransitions() {
597         return mIsReturning ? getWindow().getAllowReturnTransitionOverlap()
598                 : getWindow().getAllowEnterTransitionOverlap();
599     }
600 
startRejectedAnimations(final ArrayList<View> rejectedSnapshots)601     private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) {
602         if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) {
603             return;
604         }
605         final ViewGroup decorView = getDecor();
606         if (decorView != null) {
607             ViewGroupOverlay overlay = decorView.getOverlay();
608             ObjectAnimator animator = null;
609             int numRejected = rejectedSnapshots.size();
610             for (int i = 0; i < numRejected; i++) {
611                 View snapshot = rejectedSnapshots.get(i);
612                 overlay.add(snapshot);
613                 animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0);
614                 animator.start();
615             }
616             animator.addListener(new AnimatorListenerAdapter() {
617                 @Override
618                 public void onAnimationEnd(Animator animation) {
619                     ViewGroupOverlay overlay = decorView.getOverlay();
620                     int numRejected = rejectedSnapshots.size();
621                     for (int i = 0; i < numRejected; i++) {
622                         overlay.remove(rejectedSnapshots.get(i));
623                     }
624                 }
625             });
626         }
627     }
628 
onRemoteExitTransitionComplete()629     protected void onRemoteExitTransitionComplete() {
630         if (!allowOverlappingTransitions()) {
631             startEnterTransitionOnly();
632         }
633     }
634 
startEnterTransitionOnly()635     private void startEnterTransitionOnly() {
636         startTransition(new Runnable() {
637             @Override
638             public void run() {
639                 boolean startEnterTransition = true;
640                 boolean startSharedElementTransition = false;
641                 ViewGroup decorView = getDecor();
642                 if (decorView != null) {
643                     Transition transition = beginTransition(decorView, startEnterTransition,
644                             startSharedElementTransition);
645                     startEnterTransition(transition);
646                 }
647             }
648         });
649     }
650 
651 }
652