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