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