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