EnterTransitionCoordinator.java revision f1abef6fc2194e72af521202ee4b17c10e03c935
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.graphics.Matrix;
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.Window;
34
35import 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 */
42class 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 boolean mIsViewsTransitionComplete;
59    private boolean mIsSharedElementTransitionComplete;
60    private ArrayList<Matrix> mSharedElementParentMatrices;
61    private Transition mEnterViewsTransition;
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            setSharedElementMatrices();
126            moveSharedElementsToOverlay();
127        }
128        if (mSharedElementsBundle != null) {
129            onTakeSharedElements();
130        }
131    }
132
133    private void triggerViewsReady(final ArrayMap<String, View> sharedElements) {
134        if (mAreViewsReady) {
135            return;
136        }
137        mAreViewsReady = true;
138        // Ensure the views have been laid out before capturing the views -- we need the epicenter.
139        if (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()) {
140            viewsReady(sharedElements);
141        } else {
142            final View sharedElement = sharedElements.valueAt(0);
143            sharedElement.getViewTreeObserver()
144                    .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
145                @Override
146                public boolean onPreDraw() {
147                    sharedElement.getViewTreeObserver().removeOnPreDrawListener(this);
148                    viewsReady(sharedElements);
149                    return true;
150                }
151            });
152        }
153    }
154
155    private ArrayMap<String, View> mapNamedElements(ArrayList<String> accepted,
156            ArrayList<String> localNames) {
157        ArrayMap<String, View> sharedElements = new ArrayMap<String, View>();
158        ViewGroup decorView = getDecor();
159        if (decorView != null) {
160            decorView.findNamedViews(sharedElements);
161        }
162        if (accepted != null) {
163            for (int i = 0; i < localNames.size(); i++) {
164                String localName = localNames.get(i);
165                String acceptedName = accepted.get(i);
166                if (localName != null && !localName.equals(acceptedName)) {
167                    View view = sharedElements.remove(localName);
168                    if (view != null) {
169                        sharedElements.put(acceptedName, view);
170                    }
171                }
172            }
173        }
174        return sharedElements;
175    }
176
177    private void sendSharedElementDestination() {
178        boolean allReady;
179        final View decorView = getDecor();
180        if (allowOverlappingTransitions() && getEnterViewsTransition() != null) {
181            allReady = false;
182        } else if (decorView == null) {
183            allReady = true;
184        } else {
185            allReady = !decorView.isLayoutRequested();
186            if (allReady) {
187                for (int i = 0; i < mSharedElements.size(); i++) {
188                    if (mSharedElements.get(i).isLayoutRequested()) {
189                        allReady = false;
190                        break;
191                    }
192                }
193            }
194        }
195        if (allReady) {
196            Bundle state = captureSharedElementState();
197            setSharedElementMatrices();
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                                setSharedElementMatrices();
209                                moveSharedElementsToOverlay();
210                                mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state);
211                            }
212                            return true;
213                        }
214                    });
215        }
216        if (allowOverlappingTransitions()) {
217            startEnterTransitionOnly();
218        }
219    }
220
221    private static SharedElementCallback getListener(Activity activity, boolean isReturning) {
222        return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener;
223    }
224
225    @Override
226    protected void onReceiveResult(int resultCode, Bundle resultData) {
227        switch (resultCode) {
228            case MSG_TAKE_SHARED_ELEMENTS:
229                if (!mIsCanceled) {
230                    mSharedElementsBundle = resultData;
231                    onTakeSharedElements();
232                }
233                break;
234            case MSG_EXIT_TRANSITION_COMPLETE:
235                if (!mIsCanceled) {
236                    mIsExitTransitionComplete = true;
237                    if (mSharedElementTransitionStarted) {
238                        onRemoteExitTransitionComplete();
239                    }
240                }
241                break;
242            case MSG_CANCEL:
243                cancel();
244                break;
245        }
246    }
247
248    private void cancel() {
249        if (!mIsCanceled) {
250            mIsCanceled = true;
251            if (getViewsTransition() == null || mIsViewsTransitionStarted) {
252                showViews(mSharedElements, true);
253            } else if (mTransitioningViews != null) {
254                mTransitioningViews.addAll(mSharedElements);
255            }
256            mSharedElementNames.clear();
257            mSharedElements.clear();
258            mAllSharedElementNames.clear();
259            startSharedElementTransition(null);
260            onRemoteExitTransitionComplete();
261        }
262    }
263
264    public boolean isReturning() {
265        return mIsReturning;
266    }
267
268    protected void prepareEnter() {
269        ViewGroup decorView = getDecor();
270        if (mActivity == null || decorView == null) {
271            return;
272        }
273        mActivity.overridePendingTransition(0, 0);
274        if (!mIsReturning) {
275            mWasOpaque = mActivity.convertToTranslucent(null, null);
276            Drawable background = decorView.getBackground();
277            if (background != null) {
278                getWindow().setBackgroundDrawable(null);
279                background = background.mutate();
280                background.setAlpha(0);
281                getWindow().setBackgroundDrawable(background);
282            }
283        } else {
284            mActivity = null; // all done with it now.
285        }
286    }
287
288    @Override
289    protected Transition getViewsTransition() {
290        Window window = getWindow();
291        if (window == null) {
292            return null;
293        }
294        if (mIsReturning) {
295            return window.getReenterTransition();
296        } else {
297            return window.getEnterTransition();
298        }
299    }
300
301    protected Transition getSharedElementTransition() {
302        Window window = getWindow();
303        if (window == null) {
304            return null;
305        }
306        if (mIsReturning) {
307            return window.getSharedElementReenterTransition();
308        } else {
309            return window.getSharedElementEnterTransition();
310        }
311    }
312
313    private void startSharedElementTransition(Bundle sharedElementState) {
314        ViewGroup decorView = getDecor();
315        if (decorView == null) {
316            return;
317        }
318        // Remove rejected shared elements
319        ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames);
320        rejectedNames.removeAll(mSharedElementNames);
321        ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames);
322        if (mListener != null) {
323            mListener.onRejectSharedElements(rejectedSnapshots);
324        }
325        startRejectedAnimations(rejectedSnapshots);
326
327        // Now start shared element transition
328        ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState,
329                mSharedElementNames);
330        showViews(mSharedElements, true);
331        scheduleSetSharedElementEnd(sharedElementSnapshots);
332        ArrayList<SharedElementOriginalState> originalImageViewState =
333                setSharedElementState(sharedElementState, sharedElementSnapshots);
334        requestLayoutForSharedElements();
335
336        boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning;
337        boolean startSharedElementTransition = true;
338        setGhostVisibility(View.INVISIBLE);
339        scheduleGhostVisibilityChange(View.INVISIBLE);
340        Transition transition = beginTransition(decorView, startEnterTransition,
341                startSharedElementTransition);
342        scheduleGhostVisibilityChange(View.VISIBLE);
343        setGhostVisibility(View.VISIBLE);
344
345        if (startEnterTransition) {
346            startEnterTransition(transition);
347        }
348
349        setOriginalSharedElementState(mSharedElements, originalImageViewState);
350
351        if (mResultReceiver != null) {
352            // We can't trust that the view will disappear on the same frame that the shared
353            // element appears here. Assure that we get at least 2 frames for double-buffering.
354            decorView.postOnAnimation(new Runnable() {
355                int mAnimations;
356
357                @Override
358                public void run() {
359                    if (mAnimations++ < MIN_ANIMATION_FRAMES) {
360                        View decorView = getDecor();
361                        if (decorView != null) {
362                            decorView.postOnAnimation(this);
363                        }
364                    } else if (mResultReceiver != null) {
365                        mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null);
366                        mResultReceiver = null; // all done sending messages.
367                    }
368                }
369            });
370        }
371    }
372
373    private void onTakeSharedElements() {
374        if (!mIsReadyForTransition || mSharedElementsBundle == null) {
375            return;
376        }
377        final Bundle sharedElementState = mSharedElementsBundle;
378        mSharedElementsBundle = null;
379        final View decorView = getDecor();
380        if (decorView != null) {
381            decorView.getViewTreeObserver()
382                    .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
383                        @Override
384                        public boolean onPreDraw() {
385                            decorView.getViewTreeObserver().removeOnPreDrawListener(this);
386                            startTransition(new Runnable() {
387                                @Override
388                                public void run() {
389                                    startSharedElementTransition(sharedElementState);
390                                }
391                            });
392                            return false;
393                        }
394                    });
395            decorView.invalidate();
396        }
397    }
398
399    private void requestLayoutForSharedElements() {
400        int numSharedElements = mSharedElements.size();
401        for (int i = 0; i < numSharedElements; i++) {
402            mSharedElements.get(i).requestLayout();
403        }
404    }
405
406    private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition,
407            boolean startSharedElementTransition) {
408        Transition sharedElementTransition = null;
409        if (startSharedElementTransition) {
410            if (!mSharedElementNames.isEmpty()) {
411                sharedElementTransition = configureTransition(getSharedElementTransition(), false);
412            }
413            if (sharedElementTransition == null) {
414                sharedElementTransitionStarted();
415                sharedElementTransitionComplete();
416            } else {
417                sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() {
418                    @Override
419                    public void onTransitionStart(Transition transition) {
420                        sharedElementTransitionStarted();
421                    }
422
423                    @Override
424                    public void onTransitionEnd(Transition transition) {
425                        transition.removeListener(this);
426                        sharedElementTransitionComplete();
427                    }
428                });
429            }
430        }
431        Transition viewsTransition = null;
432        if (startEnterTransition) {
433            mIsViewsTransitionStarted = true;
434            if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
435                viewsTransition = configureTransition(getViewsTransition(), true);
436                if (viewsTransition != null && !mIsReturning) {
437                    stripOffscreenViews();
438                }
439            }
440            if (viewsTransition == null) {
441                viewTransitionComplete();
442            } else {
443                viewsTransition.forceVisibility(View.INVISIBLE, true);
444                final ArrayList<View> transitioningViews = mTransitioningViews;
445                viewsTransition.addListener(new ContinueTransitionListener() {
446                    @Override
447                    public void onTransitionStart(Transition transition) {
448                        mEnterViewsTransition = transition;
449                        if (transitioningViews != null) {
450                            showViews(transitioningViews, false);
451                        }
452                        super.onTransitionStart(transition);
453                    }
454
455                    @Override
456                    public void onTransitionEnd(Transition transition) {
457                        mEnterViewsTransition = null;
458                        transition.removeListener(this);
459                        viewTransitionComplete();
460                        super.onTransitionEnd(transition);
461                    }
462                });
463            }
464        }
465
466        Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
467        if (transition != null) {
468            transition.addListener(new ContinueTransitionListener());
469            TransitionManager.beginDelayedTransition(decorView, transition);
470            if (startSharedElementTransition && !mSharedElementNames.isEmpty()) {
471                mSharedElements.get(0).invalidate();
472            } else if (startEnterTransition && mTransitioningViews != null &&
473                    !mTransitioningViews.isEmpty()) {
474                mTransitioningViews.get(0).invalidate();
475            }
476        } else {
477            transitionStarted();
478        }
479        return transition;
480    }
481
482    private void viewTransitionComplete() {
483        mIsViewsTransitionComplete = true;
484        if (mIsSharedElementTransitionComplete) {
485            moveSharedElementsFromOverlay();
486        }
487    }
488
489    private void sharedElementTransitionComplete() {
490        mIsSharedElementTransitionComplete = true;
491        if (mIsViewsTransitionComplete) {
492            moveSharedElementsFromOverlay();
493        }
494    }
495
496    private void sharedElementTransitionStarted() {
497        mSharedElementTransitionStarted = true;
498        if (mIsExitTransitionComplete) {
499            send(MSG_EXIT_TRANSITION_COMPLETE, null);
500        }
501    }
502
503    private void startEnterTransition(Transition transition) {
504        ViewGroup decorView = getDecor();
505        if (!mIsReturning && decorView != null) {
506            Drawable background = decorView.getBackground();
507            if (background != null) {
508                background = background.mutate();
509                getWindow().setBackgroundDrawable(background);
510                mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255);
511                mBackgroundAnimator.setDuration(getFadeDuration());
512                mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
513                    @Override
514                    public void onAnimationEnd(Animator animation) {
515                        makeOpaque();
516                    }
517                });
518                mBackgroundAnimator.start();
519            } else if (transition != null) {
520                transition.addListener(new Transition.TransitionListenerAdapter() {
521                    @Override
522                    public void onTransitionEnd(Transition transition) {
523                        transition.removeListener(this);
524                        makeOpaque();
525                    }
526                });
527            } else {
528                makeOpaque();
529            }
530        }
531    }
532
533    public void stop() {
534        // Restore the background to its previous state since the
535        // Activity is stopping.
536        if (mBackgroundAnimator != null) {
537            mBackgroundAnimator.end();
538            mBackgroundAnimator = null;
539        } else if (mWasOpaque) {
540            ViewGroup decorView = getDecor();
541            if (decorView != null) {
542                Drawable drawable = decorView.getBackground();
543                if (drawable != null) {
544                    drawable.setAlpha(1);
545                }
546            }
547        }
548        makeOpaque();
549        mIsCanceled = true;
550        mResultReceiver = null;
551        mActivity = null;
552        moveSharedElementsFromOverlay();
553        if (mTransitioningViews != null) {
554            showViews(mTransitioningViews, true);
555        }
556        showViews(mSharedElements, true);
557        clearState();
558    }
559
560    public void cancelEnter() {
561        setGhostVisibility(View.INVISIBLE);
562        mHasStopped = true;
563        mIsCanceled = true;
564        mResultReceiver = null;
565        if (mBackgroundAnimator != null) {
566            mBackgroundAnimator.cancel();
567            mBackgroundAnimator = null;
568        }
569        mActivity = null;
570        clearState();
571    }
572
573    private void makeOpaque() {
574        if (!mHasStopped && mActivity != null) {
575            if (mWasOpaque) {
576                mActivity.convertFromTranslucent();
577            }
578            mActivity = null;
579        }
580    }
581
582    private boolean allowOverlappingTransitions() {
583        return mIsReturning ? getWindow().getAllowExitTransitionOverlap()
584                : getWindow().getAllowEnterTransitionOverlap();
585    }
586
587    private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) {
588        if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) {
589            return;
590        }
591        final ViewGroup decorView = getDecor();
592        if (decorView != null) {
593            ViewGroupOverlay overlay = decorView.getOverlay();
594            ObjectAnimator animator = null;
595            int numRejected = rejectedSnapshots.size();
596            for (int i = 0; i < numRejected; i++) {
597                View snapshot = rejectedSnapshots.get(i);
598                overlay.add(snapshot);
599                animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0);
600                animator.start();
601            }
602            animator.addListener(new AnimatorListenerAdapter() {
603                @Override
604                public void onAnimationEnd(Animator animation) {
605                    ViewGroupOverlay overlay = decorView.getOverlay();
606                    int numRejected = rejectedSnapshots.size();
607                    for (int i = 0; i < numRejected; i++) {
608                        overlay.remove(rejectedSnapshots.get(i));
609                    }
610                }
611            });
612        }
613    }
614
615    protected void onRemoteExitTransitionComplete() {
616        if (!allowOverlappingTransitions()) {
617            startEnterTransitionOnly();
618        }
619    }
620
621    private void startEnterTransitionOnly() {
622        startTransition(new Runnable() {
623            @Override
624            public void run() {
625                boolean startEnterTransition = true;
626                boolean startSharedElementTransition = false;
627                ViewGroup decorView = getDecor();
628                if (decorView != null) {
629                    Transition transition = beginTransition(decorView, startEnterTransition,
630                            startSharedElementTransition);
631                    startEnterTransition(transition);
632                }
633            }
634        });
635    }
636
637    private void setSharedElementMatrices() {
638        int numSharedElements = mSharedElements.size();
639        if (numSharedElements > 0) {
640            mSharedElementParentMatrices = new ArrayList<Matrix>(numSharedElements);
641        }
642        for (int i = 0; i < numSharedElements; i++) {
643            View view = mSharedElements.get(i);
644
645            // Find the location in the view's parent
646            ViewGroup parent = (ViewGroup) view.getParent();
647            Matrix matrix = new Matrix();
648            parent.transformMatrixToLocal(matrix);
649
650            mSharedElementParentMatrices.add(matrix);
651        }
652    }
653
654    @Override
655    protected void getSharedElementParentMatrix(View view, Matrix matrix) {
656        int index = mSharedElementParentMatrices == null ? -1 : mSharedElements.indexOf(view);
657        if (index < 0) {
658            super.getSharedElementParentMatrix(view, matrix);
659        } else {
660            matrix.set(mSharedElementParentMatrices.get(index));
661        }
662    }
663}
664