EnterTransitionCoordinator.java revision 6e09942a7d06adc564e6ca4a0e7328b46b4af28c
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 Transition mEnterViewsTransition;
59
60    public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver,
61            ArrayList<String> sharedElementNames, boolean isReturning) {
62        super(activity.getWindow(), sharedElementNames,
63                getListener(activity, isReturning), isReturning);
64        mActivity = activity;
65        setResultReceiver(resultReceiver);
66        prepareEnter();
67        Bundle resultReceiverBundle = new Bundle();
68        resultReceiverBundle.putParcelable(KEY_REMOTE_RECEIVER, this);
69        mResultReceiver.send(MSG_SET_REMOTE_RECEIVER, resultReceiverBundle);
70        final View decorView = getDecor();
71        if (decorView != null) {
72            decorView.getViewTreeObserver().addOnPreDrawListener(
73                    new ViewTreeObserver.OnPreDrawListener() {
74                        @Override
75                        public boolean onPreDraw() {
76                            if (mIsReadyForTransition) {
77                                decorView.getViewTreeObserver().removeOnPreDrawListener(this);
78                            }
79                            return mIsReadyForTransition;
80                        }
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 && mTransitioningViews != null) {
117            hideViews(mTransitioningViews);
118        }
119        if (mIsReturning) {
120            sendSharedElementDestination();
121        } else {
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        final ViewGroup decor = getDecor();
135        // Ensure the views have been laid out before capturing the views -- we need the epicenter.
136        if (decor == null || (decor.isAttachedToWindow() &&
137                (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()))) {
138            viewsReady(sharedElements);
139        } else {
140            decor.getViewTreeObserver()
141                    .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
142                        @Override
143                        public boolean onPreDraw() {
144                            decor.getViewTreeObserver().removeOnPreDrawListener(this);
145                            viewsReady(sharedElements);
146                            return true;
147                        }
148                    });
149        }
150    }
151
152    private ArrayMap<String, View> mapNamedElements(ArrayList<String> accepted,
153            ArrayList<String> localNames) {
154        ArrayMap<String, View> sharedElements = new ArrayMap<String, View>();
155        ViewGroup decorView = getDecor();
156        if (decorView != null) {
157            decorView.findNamedViews(sharedElements);
158        }
159        if (accepted != null) {
160            for (int i = 0; i < localNames.size(); i++) {
161                String localName = localNames.get(i);
162                String acceptedName = accepted.get(i);
163                if (localName != null && !localName.equals(acceptedName)) {
164                    View view = sharedElements.remove(localName);
165                    if (view != null) {
166                        sharedElements.put(acceptedName, view);
167                    }
168                }
169            }
170        }
171        return sharedElements;
172    }
173
174    private void sendSharedElementDestination() {
175        boolean allReady;
176        final View decorView = getDecor();
177        if (allowOverlappingTransitions() && getEnterViewsTransition() != null) {
178            allReady = false;
179        } else if (decorView == null) {
180            allReady = true;
181        } else {
182            allReady = !decorView.isLayoutRequested();
183            if (allReady) {
184                for (int i = 0; i < mSharedElements.size(); i++) {
185                    if (mSharedElements.get(i).isLayoutRequested()) {
186                        allReady = false;
187                        break;
188                    }
189                }
190            }
191        }
192        if (allReady) {
193            Bundle state = captureSharedElementState();
194            moveSharedElementsToOverlay();
195            mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state);
196        } else if (decorView != null) {
197            decorView.getViewTreeObserver()
198                    .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
199                        @Override
200                        public boolean onPreDraw() {
201                            decorView.getViewTreeObserver().removeOnPreDrawListener(this);
202                            if (mResultReceiver != null) {
203                                Bundle state = captureSharedElementState();
204                                moveSharedElementsToOverlay();
205                                mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state);
206                            }
207                            return true;
208                        }
209                    });
210        }
211        if (allowOverlappingTransitions()) {
212            startEnterTransitionOnly();
213        }
214    }
215
216    private static SharedElementCallback getListener(Activity activity, boolean isReturning) {
217        return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener;
218    }
219
220    @Override
221    protected void onReceiveResult(int resultCode, Bundle resultData) {
222        switch (resultCode) {
223            case MSG_TAKE_SHARED_ELEMENTS:
224                if (!mIsCanceled) {
225                    mSharedElementsBundle = resultData;
226                    onTakeSharedElements();
227                }
228                break;
229            case MSG_EXIT_TRANSITION_COMPLETE:
230                if (!mIsCanceled) {
231                    mIsExitTransitionComplete = true;
232                    if (mSharedElementTransitionStarted) {
233                        onRemoteExitTransitionComplete();
234                    }
235                }
236                break;
237            case MSG_CANCEL:
238                cancel();
239                break;
240        }
241    }
242
243    private void cancel() {
244        if (!mIsCanceled) {
245            mIsCanceled = true;
246            if (getViewsTransition() == null || mIsViewsTransitionStarted) {
247                showViews(mSharedElements, true);
248            } else if (mTransitioningViews != null) {
249                mTransitioningViews.addAll(mSharedElements);
250            }
251            moveSharedElementsFromOverlay();
252            mSharedElementNames.clear();
253            mSharedElements.clear();
254            mAllSharedElementNames.clear();
255            startSharedElementTransition(null);
256            onRemoteExitTransitionComplete();
257        }
258    }
259
260    public boolean isReturning() {
261        return mIsReturning;
262    }
263
264    protected void prepareEnter() {
265        ViewGroup decorView = getDecor();
266        if (mActivity == null || decorView == null) {
267            return;
268        }
269        mActivity.overridePendingTransition(0, 0);
270        if (!mIsReturning) {
271            mWasOpaque = mActivity.convertToTranslucent(null, null);
272            Drawable background = decorView.getBackground();
273            if (background != null) {
274                getWindow().setBackgroundDrawable(null);
275                background = background.mutate();
276                background.setAlpha(0);
277                getWindow().setBackgroundDrawable(background);
278            }
279        } else {
280            mActivity = null; // all done with it now.
281        }
282    }
283
284    @Override
285    protected Transition getViewsTransition() {
286        Window window = getWindow();
287        if (window == null) {
288            return null;
289        }
290        if (mIsReturning) {
291            return window.getReenterTransition();
292        } else {
293            return window.getEnterTransition();
294        }
295    }
296
297    protected Transition getSharedElementTransition() {
298        Window window = getWindow();
299        if (window == null) {
300            return null;
301        }
302        if (mIsReturning) {
303            return window.getSharedElementReenterTransition();
304        } else {
305            return window.getSharedElementEnterTransition();
306        }
307    }
308
309    private void startSharedElementTransition(Bundle sharedElementState) {
310        ViewGroup decorView = getDecor();
311        if (decorView == null) {
312            return;
313        }
314        // Remove rejected shared elements
315        ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames);
316        rejectedNames.removeAll(mSharedElementNames);
317        ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames);
318        if (mListener != null) {
319            mListener.onRejectSharedElements(rejectedSnapshots);
320        }
321        removeNullViews(rejectedSnapshots);
322        startRejectedAnimations(rejectedSnapshots);
323
324        // Now start shared element transition
325        ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState,
326                mSharedElementNames);
327        showViews(mSharedElements, true);
328        scheduleSetSharedElementEnd(sharedElementSnapshots);
329        ArrayList<SharedElementOriginalState> originalImageViewState =
330                setSharedElementState(sharedElementState, sharedElementSnapshots);
331        requestLayoutForSharedElements();
332
333        boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning;
334        boolean startSharedElementTransition = true;
335        setGhostVisibility(View.INVISIBLE);
336        scheduleGhostVisibilityChange(View.INVISIBLE);
337        pauseInput();
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                viewsTransitionComplete();
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                        viewsTransitionComplete();
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    @Override
501    protected void onTransitionsComplete() {
502        moveSharedElementsFromOverlay();
503    }
504
505    private void sharedElementTransitionStarted() {
506        mSharedElementTransitionStarted = true;
507        if (mIsExitTransitionComplete) {
508            send(MSG_EXIT_TRANSITION_COMPLETE, null);
509        }
510    }
511
512    private void startEnterTransition(Transition transition) {
513        ViewGroup decorView = getDecor();
514        if (!mIsReturning && decorView != null) {
515            Drawable background = decorView.getBackground();
516            if (background != null) {
517                background = background.mutate();
518                getWindow().setBackgroundDrawable(background);
519                mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255);
520                mBackgroundAnimator.setDuration(getFadeDuration());
521                mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
522                    @Override
523                    public void onAnimationEnd(Animator animation) {
524                        makeOpaque();
525                    }
526                });
527                mBackgroundAnimator.start();
528            } else if (transition != null) {
529                transition.addListener(new Transition.TransitionListenerAdapter() {
530                    @Override
531                    public void onTransitionEnd(Transition transition) {
532                        transition.removeListener(this);
533                        makeOpaque();
534                    }
535                });
536            } else {
537                makeOpaque();
538            }
539        }
540    }
541
542    public void stop() {
543        // Restore the background to its previous state since the
544        // Activity is stopping.
545        if (mBackgroundAnimator != null) {
546            mBackgroundAnimator.end();
547            mBackgroundAnimator = null;
548        } else if (mWasOpaque) {
549            ViewGroup decorView = getDecor();
550            if (decorView != null) {
551                Drawable drawable = decorView.getBackground();
552                if (drawable != null) {
553                    drawable.setAlpha(1);
554                }
555            }
556        }
557        makeOpaque();
558        mIsCanceled = true;
559        mResultReceiver = null;
560        mActivity = null;
561        moveSharedElementsFromOverlay();
562        if (mTransitioningViews != null) {
563            showViews(mTransitioningViews, true);
564        }
565        showViews(mSharedElements, true);
566        clearState();
567    }
568
569    /**
570     * Cancels the enter transition.
571     * @return True if the enter transition is still pending capturing the target state. If so,
572     * any transition started on the decor will do nothing.
573     */
574    public boolean cancelEnter() {
575        setGhostVisibility(View.INVISIBLE);
576        mHasStopped = true;
577        mIsCanceled = true;
578        clearState();
579        return super.cancelPendingTransitions();
580    }
581
582    @Override
583    protected void clearState() {
584        mSharedElementsBundle = null;
585        mEnterViewsTransition = null;
586        mResultReceiver = null;
587        if (mBackgroundAnimator != null) {
588            mBackgroundAnimator.cancel();
589            mBackgroundAnimator = null;
590        }
591        super.clearState();
592    }
593
594    private void makeOpaque() {
595        if (!mHasStopped && mActivity != null) {
596            if (mWasOpaque) {
597                mActivity.convertFromTranslucent();
598            }
599            mActivity = null;
600        }
601    }
602
603    private boolean allowOverlappingTransitions() {
604        return mIsReturning ? getWindow().getAllowReturnTransitionOverlap()
605                : getWindow().getAllowEnterTransitionOverlap();
606    }
607
608    private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) {
609        if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) {
610            return;
611        }
612        final ViewGroup decorView = getDecor();
613        if (decorView != null) {
614            ViewGroupOverlay overlay = decorView.getOverlay();
615            ObjectAnimator animator = null;
616            int numRejected = rejectedSnapshots.size();
617            for (int i = 0; i < numRejected; i++) {
618                View snapshot = rejectedSnapshots.get(i);
619                overlay.add(snapshot);
620                animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0);
621                animator.start();
622            }
623            animator.addListener(new AnimatorListenerAdapter() {
624                @Override
625                public void onAnimationEnd(Animator animation) {
626                    ViewGroupOverlay overlay = decorView.getOverlay();
627                    int numRejected = rejectedSnapshots.size();
628                    for (int i = 0; i < numRejected; i++) {
629                        overlay.remove(rejectedSnapshots.get(i));
630                    }
631                }
632            });
633        }
634    }
635
636    protected void onRemoteExitTransitionComplete() {
637        if (!allowOverlappingTransitions()) {
638            startEnterTransitionOnly();
639        }
640    }
641
642    private void startEnterTransitionOnly() {
643        startTransition(new Runnable() {
644            @Override
645            public void run() {
646                boolean startEnterTransition = true;
647                boolean startSharedElementTransition = false;
648                ViewGroup decorView = getDecor();
649                if (decorView != null) {
650                    Transition transition = beginTransition(decorView, startEnterTransition,
651                            startSharedElementTransition);
652                    startEnterTransition(transition);
653                }
654            }
655        });
656    }
657
658}
659