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