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