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