EnterTransitionCoordinator.java revision 10d312b5dae0ee565876101b0c914d7a1aab6451
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package android.app;
17
18import android.animation.Animator;
19import android.animation.AnimatorListenerAdapter;
20import android.animation.ObjectAnimator;
21import android.app.SharedElementCallback.OnSharedElementsReadyListener;
22import android.graphics.drawable.Drawable;
23import android.os.Bundle;
24import android.os.ResultReceiver;
25import android.text.TextUtils;
26import android.transition.Transition;
27import android.transition.TransitionManager;
28import android.util.ArrayMap;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.ViewGroupOverlay;
32import android.view.ViewTreeObserver;
33import android.view.Window;
34import android.view.accessibility.AccessibilityEvent;
35
36import java.util.ArrayList;
37
38/**
39 * This ActivityTransitionCoordinator is created by the Activity to manage
40 * the enter scene and shared element transfer into the Scene, either during
41 * launch of an Activity or returning from a launched Activity.
42 */
43class EnterTransitionCoordinator extends ActivityTransitionCoordinator {
44    private static final String TAG = "EnterTransitionCoordinator";
45
46    private static final int MIN_ANIMATION_FRAMES = 2;
47
48    private boolean mSharedElementTransitionStarted;
49    private Activity mActivity;
50    private boolean mHasStopped;
51    private boolean mIsCanceled;
52    private ObjectAnimator mBackgroundAnimator;
53    private boolean mIsExitTransitionComplete;
54    private boolean mIsReadyForTransition;
55    private Bundle mSharedElementsBundle;
56    private boolean mWasOpaque;
57    private boolean mAreViewsReady;
58    private boolean mIsViewsTransitionStarted;
59    private Transition mEnterViewsTransition;
60
61    public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver,
62            ArrayList<String> sharedElementNames, boolean isReturning) {
63        super(activity.getWindow(), sharedElementNames,
64                getListener(activity, isReturning), isReturning);
65        mActivity = activity;
66        setResultReceiver(resultReceiver);
67        prepareEnter();
68        Bundle resultReceiverBundle = new Bundle();
69        resultReceiverBundle.putParcelable(KEY_REMOTE_RECEIVER, this);
70        mResultReceiver.send(MSG_SET_REMOTE_RECEIVER, resultReceiverBundle);
71        final View decorView = getDecor();
72        if (decorView != null) {
73            decorView.getViewTreeObserver().addOnPreDrawListener(
74                    new ViewTreeObserver.OnPreDrawListener() {
75                        @Override
76                        public boolean onPreDraw() {
77                            if (mIsReadyForTransition) {
78                                decorView.getViewTreeObserver().removeOnPreDrawListener(this);
79                            }
80                            return mIsReadyForTransition;
81                        }
82                    });
83        }
84    }
85
86    public void viewInstancesReady(ArrayList<String> accepted, ArrayList<String> localNames,
87            ArrayList<View> localViews) {
88        boolean remap = false;
89        for (int i = 0; i < localViews.size(); i++) {
90            View view = localViews.get(i);
91            if (!TextUtils.equals(view.getTransitionName(), localNames.get(i))
92                    || !view.isAttachedToWindow()) {
93                remap = true;
94                break;
95            }
96        }
97        if (remap) {
98            triggerViewsReady(mapNamedElements(accepted, localNames));
99        } else {
100            triggerViewsReady(mapSharedElements(accepted, localViews));
101        }
102    }
103
104    public void namedViewsReady(ArrayList<String> accepted, ArrayList<String> localNames) {
105        triggerViewsReady(mapNamedElements(accepted, localNames));
106    }
107
108    public Transition getEnterViewsTransition() {
109        return mEnterViewsTransition;
110    }
111
112    @Override
113    protected void viewsReady(ArrayMap<String, View> sharedElements) {
114        super.viewsReady(sharedElements);
115        mIsReadyForTransition = true;
116        hideViews(mSharedElements);
117        if (getViewsTransition() != null && mTransitioningViews != null) {
118            hideViews(mTransitioningViews);
119        }
120        if (mIsReturning) {
121            sendSharedElementDestination();
122        } else {
123            moveSharedElementsToOverlay();
124        }
125        if (mSharedElementsBundle != null) {
126            onTakeSharedElements();
127        }
128    }
129
130    private void triggerViewsReady(final ArrayMap<String, View> sharedElements) {
131        if (mAreViewsReady) {
132            return;
133        }
134        mAreViewsReady = true;
135        final ViewGroup decor = getDecor();
136        // Ensure the views have been laid out before capturing the views -- we need the epicenter.
137        if (decor == null || (decor.isAttachedToWindow() &&
138                (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()))) {
139            viewsReady(sharedElements);
140        } else {
141            decor.getViewTreeObserver()
142                    .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
143                        @Override
144                        public boolean onPreDraw() {
145                            decor.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            moveSharedElementsFromOverlay();
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        pauseInput();
339        Transition transition = beginTransition(decorView, startEnterTransition,
340                startSharedElementTransition);
341        scheduleGhostVisibilityChange(View.VISIBLE);
342        setGhostVisibility(View.VISIBLE);
343
344        if (startEnterTransition) {
345            startEnterTransition(transition);
346        }
347
348        setOriginalSharedElementState(mSharedElements, originalImageViewState);
349
350        if (mResultReceiver != null) {
351            // We can't trust that the view will disappear on the same frame that the shared
352            // element appears here. Assure that we get at least 2 frames for double-buffering.
353            decorView.postOnAnimation(new Runnable() {
354                int mAnimations;
355
356                @Override
357                public void run() {
358                    if (mAnimations++ < MIN_ANIMATION_FRAMES) {
359                        View decorView = getDecor();
360                        if (decorView != null) {
361                            decorView.postOnAnimation(this);
362                        }
363                    } else if (mResultReceiver != null) {
364                        mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null);
365                        mResultReceiver = null; // all done sending messages.
366                    }
367                }
368            });
369        }
370    }
371
372    private static void removeNullViews(ArrayList<View> views) {
373        if (views != null) {
374            for (int i = views.size() - 1; i >= 0; i--) {
375                if (views.get(i) == null) {
376                    views.remove(i);
377                }
378            }
379        }
380    }
381
382    private void onTakeSharedElements() {
383        if (!mIsReadyForTransition || mSharedElementsBundle == null) {
384            return;
385        }
386        final Bundle sharedElementState = mSharedElementsBundle;
387        mSharedElementsBundle = null;
388        OnSharedElementsReadyListener listener = new OnSharedElementsReadyListener() {
389            @Override
390            public void onSharedElementsReady() {
391                final View decorView = getDecor();
392                if (decorView != null) {
393                    decorView.getViewTreeObserver()
394                            .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
395                                @Override
396                                public boolean onPreDraw() {
397                                    decorView.getViewTreeObserver().removeOnPreDrawListener(this);
398                                    startTransition(new Runnable() {
399                                        @Override
400                                        public void run() {
401                                            startSharedElementTransition(sharedElementState);
402                                        }
403                                    });
404                                    return false;
405                                }
406                            });
407                    decorView.invalidate();
408                }
409            }
410        };
411        if (mListener == null) {
412            listener.onSharedElementsReady();
413        } else {
414            mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements, listener);
415        }
416    }
417
418    private void requestLayoutForSharedElements() {
419        int numSharedElements = mSharedElements.size();
420        for (int i = 0; i < numSharedElements; i++) {
421            mSharedElements.get(i).requestLayout();
422        }
423    }
424
425    private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition,
426            boolean startSharedElementTransition) {
427        Transition sharedElementTransition = null;
428        if (startSharedElementTransition) {
429            if (!mSharedElementNames.isEmpty()) {
430                sharedElementTransition = configureTransition(getSharedElementTransition(), false);
431            }
432            if (sharedElementTransition == null) {
433                sharedElementTransitionStarted();
434                sharedElementTransitionComplete();
435            } else {
436                sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() {
437                    @Override
438                    public void onTransitionStart(Transition transition) {
439                        sharedElementTransitionStarted();
440                    }
441
442                    @Override
443                    public void onTransitionEnd(Transition transition) {
444                        transition.removeListener(this);
445                        sharedElementTransitionComplete();
446                    }
447                });
448            }
449        }
450        Transition viewsTransition = null;
451        if (startEnterTransition) {
452            mIsViewsTransitionStarted = true;
453            if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
454                viewsTransition = configureTransition(getViewsTransition(), true);
455                if (viewsTransition != null && !mIsReturning) {
456                    stripOffscreenViews();
457                }
458            }
459            if (viewsTransition == null) {
460                viewsTransitionComplete();
461            } else {
462                viewsTransition.forceVisibility(View.INVISIBLE, true);
463                final ArrayList<View> transitioningViews = mTransitioningViews;
464                viewsTransition.addListener(new ContinueTransitionListener() {
465                    @Override
466                    public void onTransitionStart(Transition transition) {
467                        mEnterViewsTransition = transition;
468                        if (transitioningViews != null) {
469                            showViews(transitioningViews, false);
470                        }
471                        super.onTransitionStart(transition);
472                    }
473
474                    @Override
475                    public void onTransitionEnd(Transition transition) {
476                        mEnterViewsTransition = null;
477                        transition.removeListener(this);
478                        viewsTransitionComplete();
479                        super.onTransitionEnd(transition);
480                    }
481                });
482            }
483        }
484
485        Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
486        if (transition != null) {
487            transition.addListener(new ContinueTransitionListener());
488            TransitionManager.beginDelayedTransition(decorView, transition);
489            if (startSharedElementTransition && !mSharedElementNames.isEmpty()) {
490                mSharedElements.get(0).invalidate();
491            } else if (startEnterTransition && mTransitioningViews != null &&
492                    !mTransitioningViews.isEmpty()) {
493                mTransitioningViews.get(0).invalidate();
494            }
495        } else {
496            transitionStarted();
497        }
498        return transition;
499    }
500
501    @Override
502    protected void onTransitionsComplete() {
503        moveSharedElementsFromOverlay();
504        final ViewGroup decorView = getDecor();
505        if (decorView != null) {
506            decorView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
507        }
508    }
509
510    private void sharedElementTransitionStarted() {
511        mSharedElementTransitionStarted = true;
512        if (mIsExitTransitionComplete) {
513            send(MSG_EXIT_TRANSITION_COMPLETE, null);
514        }
515    }
516
517    private void startEnterTransition(Transition transition) {
518        ViewGroup decorView = getDecor();
519        if (!mIsReturning && decorView != null) {
520            Drawable background = decorView.getBackground();
521            if (background != null) {
522                background = background.mutate();
523                getWindow().setBackgroundDrawable(background);
524                mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255);
525                mBackgroundAnimator.setDuration(getFadeDuration());
526                mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
527                    @Override
528                    public void onAnimationEnd(Animator animation) {
529                        makeOpaque();
530                    }
531                });
532                mBackgroundAnimator.start();
533            } else if (transition != null) {
534                transition.addListener(new Transition.TransitionListenerAdapter() {
535                    @Override
536                    public void onTransitionEnd(Transition transition) {
537                        transition.removeListener(this);
538                        makeOpaque();
539                    }
540                });
541            } else {
542                makeOpaque();
543            }
544        }
545    }
546
547    public void stop() {
548        // Restore the background to its previous state since the
549        // Activity is stopping.
550        if (mBackgroundAnimator != null) {
551            mBackgroundAnimator.end();
552            mBackgroundAnimator = null;
553        } else if (mWasOpaque) {
554            ViewGroup decorView = getDecor();
555            if (decorView != null) {
556                Drawable drawable = decorView.getBackground();
557                if (drawable != null) {
558                    drawable.setAlpha(1);
559                }
560            }
561        }
562        makeOpaque();
563        mIsCanceled = true;
564        mResultReceiver = null;
565        mActivity = null;
566        moveSharedElementsFromOverlay();
567        if (mTransitioningViews != null) {
568            showViews(mTransitioningViews, true);
569        }
570        showViews(mSharedElements, true);
571        clearState();
572    }
573
574    /**
575     * Cancels the enter transition.
576     * @return True if the enter transition is still pending capturing the target state. If so,
577     * any transition started on the decor will do nothing.
578     */
579    public boolean cancelEnter() {
580        setGhostVisibility(View.INVISIBLE);
581        mHasStopped = true;
582        mIsCanceled = true;
583        clearState();
584        return super.cancelPendingTransitions();
585    }
586
587    @Override
588    protected void clearState() {
589        mSharedElementsBundle = null;
590        mEnterViewsTransition = null;
591        mResultReceiver = null;
592        if (mBackgroundAnimator != null) {
593            mBackgroundAnimator.cancel();
594            mBackgroundAnimator = null;
595        }
596        super.clearState();
597    }
598
599    private void makeOpaque() {
600        if (!mHasStopped && mActivity != null) {
601            if (mWasOpaque) {
602                mActivity.convertFromTranslucent();
603            }
604            mActivity = null;
605        }
606    }
607
608    private boolean allowOverlappingTransitions() {
609        return mIsReturning ? getWindow().getAllowReturnTransitionOverlap()
610                : getWindow().getAllowEnterTransitionOverlap();
611    }
612
613    private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) {
614        if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) {
615            return;
616        }
617        final ViewGroup decorView = getDecor();
618        if (decorView != null) {
619            ViewGroupOverlay overlay = decorView.getOverlay();
620            ObjectAnimator animator = null;
621            int numRejected = rejectedSnapshots.size();
622            for (int i = 0; i < numRejected; i++) {
623                View snapshot = rejectedSnapshots.get(i);
624                overlay.add(snapshot);
625                animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0);
626                animator.start();
627            }
628            animator.addListener(new AnimatorListenerAdapter() {
629                @Override
630                public void onAnimationEnd(Animator animation) {
631                    ViewGroupOverlay overlay = decorView.getOverlay();
632                    int numRejected = rejectedSnapshots.size();
633                    for (int i = 0; i < numRejected; i++) {
634                        overlay.remove(rejectedSnapshots.get(i));
635                    }
636                }
637            });
638        }
639    }
640
641    protected void onRemoteExitTransitionComplete() {
642        if (!allowOverlappingTransitions()) {
643            startEnterTransitionOnly();
644        }
645    }
646
647    private void startEnterTransitionOnly() {
648        startTransition(new Runnable() {
649            @Override
650            public void run() {
651                boolean startEnterTransition = true;
652                boolean startSharedElementTransition = false;
653                ViewGroup decorView = getDecor();
654                if (decorView != null) {
655                    Transition transition = beginTransition(decorView, startEnterTransition,
656                            startSharedElementTransition);
657                    startEnterTransition(transition);
658                }
659            }
660        });
661    }
662
663}
664