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