EnterTransitionCoordinator.java revision bd93e69c8e0b06fc0b0bbba4bdd9e426b37f98e1
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;
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        OnSharedElementsReadyListener listener = new OnSharedElementsReadyListener() {
388            @Override
389            public void onSharedElementsReady() {
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        if (mListener == null) {
411            listener.onSharedElementsReady();
412        } else {
413            mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements, listener);
414        }
415    }
416
417    private void requestLayoutForSharedElements() {
418        int numSharedElements = mSharedElements.size();
419        for (int i = 0; i < numSharedElements; i++) {
420            mSharedElements.get(i).requestLayout();
421        }
422    }
423
424    private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition,
425            boolean startSharedElementTransition) {
426        Transition sharedElementTransition = null;
427        if (startSharedElementTransition) {
428            if (!mSharedElementNames.isEmpty()) {
429                sharedElementTransition = configureTransition(getSharedElementTransition(), false);
430            }
431            if (sharedElementTransition == null) {
432                sharedElementTransitionStarted();
433                sharedElementTransitionComplete();
434            } else {
435                sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() {
436                    @Override
437                    public void onTransitionStart(Transition transition) {
438                        sharedElementTransitionStarted();
439                    }
440
441                    @Override
442                    public void onTransitionEnd(Transition transition) {
443                        transition.removeListener(this);
444                        sharedElementTransitionComplete();
445                    }
446                });
447            }
448        }
449        Transition viewsTransition = null;
450        if (startEnterTransition) {
451            mIsViewsTransitionStarted = true;
452            if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
453                viewsTransition = configureTransition(getViewsTransition(), true);
454                if (viewsTransition != null && !mIsReturning) {
455                    stripOffscreenViews();
456                }
457            }
458            if (viewsTransition == null) {
459                viewTransitionComplete();
460            } else {
461                viewsTransition.forceVisibility(View.INVISIBLE, true);
462                final ArrayList<View> transitioningViews = mTransitioningViews;
463                viewsTransition.addListener(new ContinueTransitionListener() {
464                    @Override
465                    public void onTransitionStart(Transition transition) {
466                        mEnterViewsTransition = transition;
467                        if (transitioningViews != null) {
468                            showViews(transitioningViews, false);
469                        }
470                        super.onTransitionStart(transition);
471                    }
472
473                    @Override
474                    public void onTransitionEnd(Transition transition) {
475                        mEnterViewsTransition = null;
476                        transition.removeListener(this);
477                        viewTransitionComplete();
478                        super.onTransitionEnd(transition);
479                    }
480                });
481            }
482        }
483
484        Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
485        if (transition != null) {
486            transition.addListener(new ContinueTransitionListener());
487            TransitionManager.beginDelayedTransition(decorView, transition);
488            if (startSharedElementTransition && !mSharedElementNames.isEmpty()) {
489                mSharedElements.get(0).invalidate();
490            } else if (startEnterTransition && mTransitioningViews != null &&
491                    !mTransitioningViews.isEmpty()) {
492                mTransitioningViews.get(0).invalidate();
493            }
494        } else {
495            transitionStarted();
496        }
497        return transition;
498    }
499
500    private void viewTransitionComplete() {
501        mIsViewsTransitionComplete = true;
502        if (mIsSharedElementTransitionComplete) {
503            moveSharedElementsFromOverlay();
504        }
505    }
506
507    private void sharedElementTransitionComplete() {
508        mIsSharedElementTransitionComplete = true;
509        if (mIsViewsTransitionComplete) {
510            moveSharedElementsFromOverlay();
511        }
512    }
513
514    private void sharedElementTransitionStarted() {
515        mSharedElementTransitionStarted = true;
516        if (mIsExitTransitionComplete) {
517            send(MSG_EXIT_TRANSITION_COMPLETE, null);
518        }
519    }
520
521    private void startEnterTransition(Transition transition) {
522        ViewGroup decorView = getDecor();
523        if (!mIsReturning && decorView != null) {
524            Drawable background = decorView.getBackground();
525            if (background != null) {
526                background = background.mutate();
527                getWindow().setBackgroundDrawable(background);
528                mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255);
529                mBackgroundAnimator.setDuration(getFadeDuration());
530                mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
531                    @Override
532                    public void onAnimationEnd(Animator animation) {
533                        makeOpaque();
534                    }
535                });
536                mBackgroundAnimator.start();
537            } else if (transition != null) {
538                transition.addListener(new Transition.TransitionListenerAdapter() {
539                    @Override
540                    public void onTransitionEnd(Transition transition) {
541                        transition.removeListener(this);
542                        makeOpaque();
543                    }
544                });
545            } else {
546                makeOpaque();
547            }
548        }
549    }
550
551    public void stop() {
552        // Restore the background to its previous state since the
553        // Activity is stopping.
554        if (mBackgroundAnimator != null) {
555            mBackgroundAnimator.end();
556            mBackgroundAnimator = null;
557        } else if (mWasOpaque) {
558            ViewGroup decorView = getDecor();
559            if (decorView != null) {
560                Drawable drawable = decorView.getBackground();
561                if (drawable != null) {
562                    drawable.setAlpha(1);
563                }
564            }
565        }
566        makeOpaque();
567        mIsCanceled = true;
568        mResultReceiver = null;
569        mActivity = null;
570        moveSharedElementsFromOverlay();
571        if (mTransitioningViews != null) {
572            showViews(mTransitioningViews, true);
573        }
574        showViews(mSharedElements, true);
575        clearState();
576    }
577
578    /**
579     * Cancels the enter transition.
580     * @return True if the enter transition is still pending capturing the target state. If so,
581     * any transition started on the decor will do nothing.
582     */
583    public boolean cancelEnter() {
584        setGhostVisibility(View.INVISIBLE);
585        mHasStopped = true;
586        mIsCanceled = true;
587        mResultReceiver = null;
588        if (mBackgroundAnimator != null) {
589            mBackgroundAnimator.cancel();
590            mBackgroundAnimator = null;
591        }
592        mActivity = null;
593        clearState();
594        return super.cancelPendingTransitions();
595    }
596
597    private void makeOpaque() {
598        if (!mHasStopped && mActivity != null) {
599            if (mWasOpaque) {
600                mActivity.convertFromTranslucent();
601            }
602            mActivity = null;
603        }
604    }
605
606    private boolean allowOverlappingTransitions() {
607        return mIsReturning ? getWindow().getAllowExitTransitionOverlap()
608                : getWindow().getAllowEnterTransitionOverlap();
609    }
610
611    private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) {
612        if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) {
613            return;
614        }
615        final ViewGroup decorView = getDecor();
616        if (decorView != null) {
617            ViewGroupOverlay overlay = decorView.getOverlay();
618            ObjectAnimator animator = null;
619            int numRejected = rejectedSnapshots.size();
620            for (int i = 0; i < numRejected; i++) {
621                View snapshot = rejectedSnapshots.get(i);
622                overlay.add(snapshot);
623                animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0);
624                animator.start();
625            }
626            animator.addListener(new AnimatorListenerAdapter() {
627                @Override
628                public void onAnimationEnd(Animator animation) {
629                    ViewGroupOverlay overlay = decorView.getOverlay();
630                    int numRejected = rejectedSnapshots.size();
631                    for (int i = 0; i < numRejected; i++) {
632                        overlay.remove(rejectedSnapshots.get(i));
633                    }
634                }
635            });
636        }
637    }
638
639    protected void onRemoteExitTransitionComplete() {
640        if (!allowOverlappingTransitions()) {
641            startEnterTransitionOnly();
642        }
643    }
644
645    private void startEnterTransitionOnly() {
646        startTransition(new Runnable() {
647            @Override
648            public void run() {
649                boolean startEnterTransition = true;
650                boolean startSharedElementTransition = false;
651                ViewGroup decorView = getDecor();
652                if (decorView != null) {
653                    Transition transition = beginTransition(decorView, startEnterTransition,
654                            startSharedElementTransition);
655                    startEnterTransition(transition);
656                }
657            }
658        });
659    }
660
661}
662