EnterTransitionCoordinator.java revision 83c692efd3c53050fce132dfd2ef21763d3cf010
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        removeNullViews(rejectedSnapshots);
326        startRejectedAnimations(rejectedSnapshots);
327
328        // Now start shared element transition
329        ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState,
330                mSharedElementNames);
331        showViews(mSharedElements, true);
332        scheduleSetSharedElementEnd(sharedElementSnapshots);
333        ArrayList<SharedElementOriginalState> originalImageViewState =
334                setSharedElementState(sharedElementState, sharedElementSnapshots);
335        requestLayoutForSharedElements();
336
337        boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning;
338        boolean startSharedElementTransition = true;
339        setGhostVisibility(View.INVISIBLE);
340        scheduleGhostVisibilityChange(View.INVISIBLE);
341        Transition transition = beginTransition(decorView, startEnterTransition,
342                startSharedElementTransition);
343        scheduleGhostVisibilityChange(View.VISIBLE);
344        setGhostVisibility(View.VISIBLE);
345
346        if (startEnterTransition) {
347            startEnterTransition(transition);
348        }
349
350        setOriginalSharedElementState(mSharedElements, originalImageViewState);
351
352        if (mResultReceiver != null) {
353            // We can't trust that the view will disappear on the same frame that the shared
354            // element appears here. Assure that we get at least 2 frames for double-buffering.
355            decorView.postOnAnimation(new Runnable() {
356                int mAnimations;
357
358                @Override
359                public void run() {
360                    if (mAnimations++ < MIN_ANIMATION_FRAMES) {
361                        View decorView = getDecor();
362                        if (decorView != null) {
363                            decorView.postOnAnimation(this);
364                        }
365                    } else if (mResultReceiver != null) {
366                        mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null);
367                        mResultReceiver = null; // all done sending messages.
368                    }
369                }
370            });
371        }
372    }
373
374    private static void removeNullViews(ArrayList<View> views) {
375        if (views != null) {
376            for (int i = views.size() - 1; i >= 0; i--) {
377                if (views.get(i) == null) {
378                    views.remove(i);
379                }
380            }
381        }
382    }
383
384    private void onTakeSharedElements() {
385        if (!mIsReadyForTransition || mSharedElementsBundle == null) {
386            return;
387        }
388        final Bundle sharedElementState = mSharedElementsBundle;
389        mSharedElementsBundle = null;
390        final View decorView = getDecor();
391        if (decorView != null) {
392            decorView.getViewTreeObserver()
393                    .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
394                        @Override
395                        public boolean onPreDraw() {
396                            decorView.getViewTreeObserver().removeOnPreDrawListener(this);
397                            startTransition(new Runnable() {
398                                @Override
399                                public void run() {
400                                    startSharedElementTransition(sharedElementState);
401                                }
402                            });
403                            return false;
404                        }
405                    });
406            decorView.invalidate();
407        }
408    }
409
410    private void requestLayoutForSharedElements() {
411        int numSharedElements = mSharedElements.size();
412        for (int i = 0; i < numSharedElements; i++) {
413            mSharedElements.get(i).requestLayout();
414        }
415    }
416
417    private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition,
418            boolean startSharedElementTransition) {
419        Transition sharedElementTransition = null;
420        if (startSharedElementTransition) {
421            if (!mSharedElementNames.isEmpty()) {
422                sharedElementTransition = configureTransition(getSharedElementTransition(), false);
423            }
424            if (sharedElementTransition == null) {
425                sharedElementTransitionStarted();
426                sharedElementTransitionComplete();
427            } else {
428                sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() {
429                    @Override
430                    public void onTransitionStart(Transition transition) {
431                        sharedElementTransitionStarted();
432                    }
433
434                    @Override
435                    public void onTransitionEnd(Transition transition) {
436                        transition.removeListener(this);
437                        sharedElementTransitionComplete();
438                    }
439                });
440            }
441        }
442        Transition viewsTransition = null;
443        if (startEnterTransition) {
444            mIsViewsTransitionStarted = true;
445            if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
446                viewsTransition = configureTransition(getViewsTransition(), true);
447                if (viewsTransition != null && !mIsReturning) {
448                    stripOffscreenViews();
449                }
450            }
451            if (viewsTransition == null) {
452                viewTransitionComplete();
453            } else {
454                viewsTransition.forceVisibility(View.INVISIBLE, true);
455                final ArrayList<View> transitioningViews = mTransitioningViews;
456                viewsTransition.addListener(new ContinueTransitionListener() {
457                    @Override
458                    public void onTransitionStart(Transition transition) {
459                        mEnterViewsTransition = transition;
460                        if (transitioningViews != null) {
461                            showViews(transitioningViews, false);
462                        }
463                        super.onTransitionStart(transition);
464                    }
465
466                    @Override
467                    public void onTransitionEnd(Transition transition) {
468                        mEnterViewsTransition = null;
469                        transition.removeListener(this);
470                        viewTransitionComplete();
471                        super.onTransitionEnd(transition);
472                    }
473                });
474            }
475        }
476
477        Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
478        if (transition != null) {
479            transition.addListener(new ContinueTransitionListener());
480            TransitionManager.beginDelayedTransition(decorView, transition);
481            if (startSharedElementTransition && !mSharedElementNames.isEmpty()) {
482                mSharedElements.get(0).invalidate();
483            } else if (startEnterTransition && mTransitioningViews != null &&
484                    !mTransitioningViews.isEmpty()) {
485                mTransitioningViews.get(0).invalidate();
486            }
487        } else {
488            transitionStarted();
489        }
490        return transition;
491    }
492
493    private void viewTransitionComplete() {
494        mIsViewsTransitionComplete = true;
495        if (mIsSharedElementTransitionComplete) {
496            moveSharedElementsFromOverlay();
497        }
498    }
499
500    private void sharedElementTransitionComplete() {
501        mIsSharedElementTransitionComplete = true;
502        if (mIsViewsTransitionComplete) {
503            moveSharedElementsFromOverlay();
504        }
505    }
506
507    private void sharedElementTransitionStarted() {
508        mSharedElementTransitionStarted = true;
509        if (mIsExitTransitionComplete) {
510            send(MSG_EXIT_TRANSITION_COMPLETE, null);
511        }
512    }
513
514    private void startEnterTransition(Transition transition) {
515        ViewGroup decorView = getDecor();
516        if (!mIsReturning && decorView != null) {
517            Drawable background = decorView.getBackground();
518            if (background != null) {
519                background = background.mutate();
520                getWindow().setBackgroundDrawable(background);
521                mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255);
522                mBackgroundAnimator.setDuration(getFadeDuration());
523                mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
524                    @Override
525                    public void onAnimationEnd(Animator animation) {
526                        makeOpaque();
527                    }
528                });
529                mBackgroundAnimator.start();
530            } else if (transition != null) {
531                transition.addListener(new Transition.TransitionListenerAdapter() {
532                    @Override
533                    public void onTransitionEnd(Transition transition) {
534                        transition.removeListener(this);
535                        makeOpaque();
536                    }
537                });
538            } else {
539                makeOpaque();
540            }
541        }
542    }
543
544    public void stop() {
545        // Restore the background to its previous state since the
546        // Activity is stopping.
547        if (mBackgroundAnimator != null) {
548            mBackgroundAnimator.end();
549            mBackgroundAnimator = null;
550        } else if (mWasOpaque) {
551            ViewGroup decorView = getDecor();
552            if (decorView != null) {
553                Drawable drawable = decorView.getBackground();
554                if (drawable != null) {
555                    drawable.setAlpha(1);
556                }
557            }
558        }
559        makeOpaque();
560        mIsCanceled = true;
561        mResultReceiver = null;
562        mActivity = null;
563        moveSharedElementsFromOverlay();
564        if (mTransitioningViews != null) {
565            showViews(mTransitioningViews, true);
566        }
567        showViews(mSharedElements, true);
568        clearState();
569    }
570
571    public void cancelEnter() {
572        setGhostVisibility(View.INVISIBLE);
573        mHasStopped = true;
574        mIsCanceled = true;
575        mResultReceiver = null;
576        if (mBackgroundAnimator != null) {
577            mBackgroundAnimator.cancel();
578            mBackgroundAnimator = null;
579        }
580        mActivity = null;
581        clearState();
582    }
583
584    private void makeOpaque() {
585        if (!mHasStopped && mActivity != null) {
586            if (mWasOpaque) {
587                mActivity.convertFromTranslucent();
588            }
589            mActivity = null;
590        }
591    }
592
593    private boolean allowOverlappingTransitions() {
594        return mIsReturning ? getWindow().getAllowExitTransitionOverlap()
595                : getWindow().getAllowEnterTransitionOverlap();
596    }
597
598    private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) {
599        if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) {
600            return;
601        }
602        final ViewGroup decorView = getDecor();
603        if (decorView != null) {
604            ViewGroupOverlay overlay = decorView.getOverlay();
605            ObjectAnimator animator = null;
606            int numRejected = rejectedSnapshots.size();
607            for (int i = 0; i < numRejected; i++) {
608                View snapshot = rejectedSnapshots.get(i);
609                overlay.add(snapshot);
610                animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0);
611                animator.start();
612            }
613            animator.addListener(new AnimatorListenerAdapter() {
614                @Override
615                public void onAnimationEnd(Animator animation) {
616                    ViewGroupOverlay overlay = decorView.getOverlay();
617                    int numRejected = rejectedSnapshots.size();
618                    for (int i = 0; i < numRejected; i++) {
619                        overlay.remove(rejectedSnapshots.get(i));
620                    }
621                }
622            });
623        }
624    }
625
626    protected void onRemoteExitTransitionComplete() {
627        if (!allowOverlappingTransitions()) {
628            startEnterTransitionOnly();
629        }
630    }
631
632    private void startEnterTransitionOnly() {
633        startTransition(new Runnable() {
634            @Override
635            public void run() {
636                boolean startEnterTransition = true;
637                boolean startSharedElementTransition = false;
638                ViewGroup decorView = getDecor();
639                if (decorView != null) {
640                    Transition transition = beginTransition(decorView, startEnterTransition,
641                            startSharedElementTransition);
642                    startEnterTransition(transition);
643                }
644            }
645        });
646    }
647
648    private void setSharedElementMatrices() {
649        int numSharedElements = mSharedElements.size();
650        if (numSharedElements > 0) {
651            mSharedElementParentMatrices = new ArrayList<Matrix>(numSharedElements);
652        }
653        for (int i = 0; i < numSharedElements; i++) {
654            View view = mSharedElements.get(i);
655
656            // Find the location in the view's parent
657            ViewGroup parent = (ViewGroup) view.getParent();
658            Matrix matrix = new Matrix();
659            parent.transformMatrixToLocal(matrix);
660
661            mSharedElementParentMatrices.add(matrix);
662        }
663    }
664
665    @Override
666    protected void getSharedElementParentMatrix(View view, Matrix matrix) {
667        int index = mSharedElementParentMatrices == null ? -1 : mSharedElements.indexOf(view);
668        if (index < 0) {
669            super.getSharedElementParentMatrix(view, matrix);
670        } else {
671            matrix.set(mSharedElementParentMatrices.get(index));
672        }
673    }
674}
675