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