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.drawable.Drawable;
22import android.os.Bundle;
23import android.os.ResultReceiver;
24import android.text.TextUtils;
25import android.transition.Transition;
26import android.transition.TransitionManager;
27import android.util.ArrayMap;
28import android.view.View;
29import android.view.ViewGroup;
30import android.view.ViewGroupOverlay;
31import android.view.ViewTreeObserver;
32import android.view.Window;
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 Transition mEnterViewsTransition;
60
61    public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver,
62            ArrayList<String> sharedElementNames, boolean isReturning) {
63        super(activity.getWindow(), sharedElementNames,
64                getListener(activity, isReturning), isReturning);
65        mActivity = activity;
66        setResultReceiver(resultReceiver);
67        prepareEnter();
68        Bundle resultReceiverBundle = new Bundle();
69        resultReceiverBundle.putParcelable(KEY_REMOTE_RECEIVER, this);
70        mResultReceiver.send(MSG_SET_REMOTE_RECEIVER, resultReceiverBundle);
71        final View decorView = getDecor();
72        if (decorView != null) {
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
86    public void viewInstancesReady(ArrayList<String> accepted, ArrayList<String> localNames,
87            ArrayList<View> localViews) {
88        boolean remap = false;
89        for (int i = 0; i < localViews.size(); i++) {
90            View view = localViews.get(i);
91            if (!TextUtils.equals(view.getTransitionName(), localNames.get(i))
92                    || !view.isAttachedToWindow()) {
93                remap = true;
94                break;
95            }
96        }
97        if (remap) {
98            triggerViewsReady(mapNamedElements(accepted, localNames));
99        } else {
100            triggerViewsReady(mapSharedElements(accepted, localViews));
101        }
102    }
103
104    public void namedViewsReady(ArrayList<String> accepted, ArrayList<String> localNames) {
105        triggerViewsReady(mapNamedElements(accepted, localNames));
106    }
107
108    public Transition getEnterViewsTransition() {
109        return mEnterViewsTransition;
110    }
111
112    @Override
113    protected void viewsReady(ArrayMap<String, View> sharedElements) {
114        super.viewsReady(sharedElements);
115        mIsReadyForTransition = true;
116        hideViews(mSharedElements);
117        if (getViewsTransition() != null && mTransitioningViews != null) {
118            hideViews(mTransitioningViews);
119        }
120        if (mIsReturning) {
121            sendSharedElementDestination();
122        } else {
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        final ViewGroup decor = getDecor();
136        // Ensure the views have been laid out before capturing the views -- we need the epicenter.
137        if (decor == null || (decor.isAttachedToWindow() &&
138                (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()))) {
139            viewsReady(sharedElements);
140        } else {
141            decor.getViewTreeObserver()
142                    .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
143                @Override
144                public boolean onPreDraw() {
145                    decor.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        ViewGroup decorView = getDecor();
157        if (decorView != null) {
158            decorView.findNamedViews(sharedElements);
159        }
160        if (accepted != null) {
161            for (int i = 0; i < localNames.size(); i++) {
162                String localName = localNames.get(i);
163                String acceptedName = accepted.get(i);
164                if (localName != null && !localName.equals(acceptedName)) {
165                    View view = sharedElements.remove(localName);
166                    if (view != null) {
167                        sharedElements.put(acceptedName, view);
168                    }
169                }
170            }
171        }
172        return sharedElements;
173    }
174
175    private void sendSharedElementDestination() {
176        boolean allReady;
177        final View decorView = getDecor();
178        if (allowOverlappingTransitions() && getEnterViewsTransition() != null) {
179            allReady = false;
180        } else if (decorView == null) {
181            allReady = true;
182        } else {
183            allReady = !decorView.isLayoutRequested();
184            if (allReady) {
185                for (int i = 0; i < mSharedElements.size(); i++) {
186                    if (mSharedElements.get(i).isLayoutRequested()) {
187                        allReady = false;
188                        break;
189                    }
190                }
191            }
192        }
193        if (allReady) {
194            Bundle state = captureSharedElementState();
195            moveSharedElementsToOverlay();
196            mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state);
197        } else if (decorView != null) {
198            decorView.getViewTreeObserver()
199                    .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
200                        @Override
201                        public boolean onPreDraw() {
202                            decorView.getViewTreeObserver().removeOnPreDrawListener(this);
203                            if (mResultReceiver != null) {
204                                Bundle state = captureSharedElementState();
205                                moveSharedElementsToOverlay();
206                                mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state);
207                            }
208                            return true;
209                        }
210                    });
211        }
212        if (allowOverlappingTransitions()) {
213            startEnterTransitionOnly();
214        }
215    }
216
217    private static SharedElementCallback getListener(Activity activity, boolean isReturning) {
218        return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener;
219    }
220
221    @Override
222    protected void onReceiveResult(int resultCode, Bundle resultData) {
223        switch (resultCode) {
224            case MSG_TAKE_SHARED_ELEMENTS:
225                if (!mIsCanceled) {
226                    mSharedElementsBundle = resultData;
227                    onTakeSharedElements();
228                }
229                break;
230            case MSG_EXIT_TRANSITION_COMPLETE:
231                if (!mIsCanceled) {
232                    mIsExitTransitionComplete = true;
233                    if (mSharedElementTransitionStarted) {
234                        onRemoteExitTransitionComplete();
235                    }
236                }
237                break;
238            case MSG_CANCEL:
239                cancel();
240                break;
241        }
242    }
243
244    private void cancel() {
245        if (!mIsCanceled) {
246            mIsCanceled = true;
247            if (getViewsTransition() == null || mIsViewsTransitionStarted) {
248                showViews(mSharedElements, true);
249            } else if (mTransitioningViews != null) {
250                mTransitioningViews.addAll(mSharedElements);
251            }
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        Transition transition = beginTransition(decorView, startEnterTransition,
338                startSharedElementTransition);
339        scheduleGhostVisibilityChange(View.VISIBLE);
340        setGhostVisibility(View.VISIBLE);
341
342        if (startEnterTransition) {
343            startEnterTransition(transition);
344        }
345
346        setOriginalSharedElementState(mSharedElements, originalImageViewState);
347
348        if (mResultReceiver != null) {
349            // We can't trust that the view will disappear on the same frame that the shared
350            // element appears here. Assure that we get at least 2 frames for double-buffering.
351            decorView.postOnAnimation(new Runnable() {
352                int mAnimations;
353
354                @Override
355                public void run() {
356                    if (mAnimations++ < MIN_ANIMATION_FRAMES) {
357                        View decorView = getDecor();
358                        if (decorView != null) {
359                            decorView.postOnAnimation(this);
360                        }
361                    } else if (mResultReceiver != null) {
362                        mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null);
363                        mResultReceiver = null; // all done sending messages.
364                    }
365                }
366            });
367        }
368    }
369
370    private static void removeNullViews(ArrayList<View> views) {
371        if (views != null) {
372            for (int i = views.size() - 1; i >= 0; i--) {
373                if (views.get(i) == null) {
374                    views.remove(i);
375                }
376            }
377        }
378    }
379
380    private void onTakeSharedElements() {
381        if (!mIsReadyForTransition || mSharedElementsBundle == null) {
382            return;
383        }
384        final Bundle sharedElementState = mSharedElementsBundle;
385        mSharedElementsBundle = null;
386        final View decorView = getDecor();
387        if (decorView != null) {
388            decorView.getViewTreeObserver()
389                    .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
390                        @Override
391                        public boolean onPreDraw() {
392                            decorView.getViewTreeObserver().removeOnPreDrawListener(this);
393                            startTransition(new Runnable() {
394                                @Override
395                                public void run() {
396                                    startSharedElementTransition(sharedElementState);
397                                }
398                            });
399                            return false;
400                        }
401                    });
402            decorView.invalidate();
403        }
404    }
405
406    private void requestLayoutForSharedElements() {
407        int numSharedElements = mSharedElements.size();
408        for (int i = 0; i < numSharedElements; i++) {
409            mSharedElements.get(i).requestLayout();
410        }
411    }
412
413    private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition,
414            boolean startSharedElementTransition) {
415        Transition sharedElementTransition = null;
416        if (startSharedElementTransition) {
417            if (!mSharedElementNames.isEmpty()) {
418                sharedElementTransition = configureTransition(getSharedElementTransition(), false);
419            }
420            if (sharedElementTransition == null) {
421                sharedElementTransitionStarted();
422                sharedElementTransitionComplete();
423            } else {
424                sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() {
425                    @Override
426                    public void onTransitionStart(Transition transition) {
427                        sharedElementTransitionStarted();
428                    }
429
430                    @Override
431                    public void onTransitionEnd(Transition transition) {
432                        transition.removeListener(this);
433                        sharedElementTransitionComplete();
434                    }
435                });
436            }
437        }
438        Transition viewsTransition = null;
439        if (startEnterTransition) {
440            mIsViewsTransitionStarted = true;
441            if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
442                viewsTransition = configureTransition(getViewsTransition(), true);
443                if (viewsTransition != null && !mIsReturning) {
444                    stripOffscreenViews();
445                }
446            }
447            if (viewsTransition == null) {
448                viewTransitionComplete();
449            } else {
450                viewsTransition.forceVisibility(View.INVISIBLE, true);
451                final ArrayList<View> transitioningViews = mTransitioningViews;
452                viewsTransition.addListener(new ContinueTransitionListener() {
453                    @Override
454                    public void onTransitionStart(Transition transition) {
455                        mEnterViewsTransition = transition;
456                        if (transitioningViews != null) {
457                            showViews(transitioningViews, false);
458                        }
459                        super.onTransitionStart(transition);
460                    }
461
462                    @Override
463                    public void onTransitionEnd(Transition transition) {
464                        mEnterViewsTransition = null;
465                        transition.removeListener(this);
466                        viewTransitionComplete();
467                        super.onTransitionEnd(transition);
468                    }
469                });
470            }
471        }
472
473        Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
474        if (transition != null) {
475            transition.addListener(new ContinueTransitionListener());
476            TransitionManager.beginDelayedTransition(decorView, transition);
477            if (startSharedElementTransition && !mSharedElementNames.isEmpty()) {
478                mSharedElements.get(0).invalidate();
479            } else if (startEnterTransition && mTransitioningViews != null &&
480                    !mTransitioningViews.isEmpty()) {
481                mTransitioningViews.get(0).invalidate();
482            }
483        } else {
484            transitionStarted();
485        }
486        return transition;
487    }
488
489    private void viewTransitionComplete() {
490        mIsViewsTransitionComplete = true;
491        if (mIsSharedElementTransitionComplete) {
492            moveSharedElementsFromOverlay();
493        }
494    }
495
496    private void sharedElementTransitionComplete() {
497        mIsSharedElementTransitionComplete = true;
498        if (mIsViewsTransitionComplete) {
499            moveSharedElementsFromOverlay();
500        }
501    }
502
503    private void sharedElementTransitionStarted() {
504        mSharedElementTransitionStarted = true;
505        if (mIsExitTransitionComplete) {
506            send(MSG_EXIT_TRANSITION_COMPLETE, null);
507        }
508    }
509
510    private void startEnterTransition(Transition transition) {
511        ViewGroup decorView = getDecor();
512        if (!mIsReturning && decorView != null) {
513            Drawable background = decorView.getBackground();
514            if (background != null) {
515                background = background.mutate();
516                getWindow().setBackgroundDrawable(background);
517                mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255);
518                mBackgroundAnimator.setDuration(getFadeDuration());
519                mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
520                    @Override
521                    public void onAnimationEnd(Animator animation) {
522                        makeOpaque();
523                    }
524                });
525                mBackgroundAnimator.start();
526            } else if (transition != null) {
527                transition.addListener(new Transition.TransitionListenerAdapter() {
528                    @Override
529                    public void onTransitionEnd(Transition transition) {
530                        transition.removeListener(this);
531                        makeOpaque();
532                    }
533                });
534            } else {
535                makeOpaque();
536            }
537        }
538    }
539
540    public void stop() {
541        // Restore the background to its previous state since the
542        // Activity is stopping.
543        if (mBackgroundAnimator != null) {
544            mBackgroundAnimator.end();
545            mBackgroundAnimator = null;
546        } else if (mWasOpaque) {
547            ViewGroup decorView = getDecor();
548            if (decorView != null) {
549                Drawable drawable = decorView.getBackground();
550                if (drawable != null) {
551                    drawable.setAlpha(1);
552                }
553            }
554        }
555        makeOpaque();
556        mIsCanceled = true;
557        mResultReceiver = null;
558        mActivity = null;
559        moveSharedElementsFromOverlay();
560        if (mTransitioningViews != null) {
561            showViews(mTransitioningViews, true);
562        }
563        showViews(mSharedElements, true);
564        clearState();
565    }
566
567    /**
568     * Cancels the enter transition.
569     * @return True if the enter transition is still pending capturing the target state. If so,
570     * any transition started on the decor will do nothing.
571     */
572    public boolean cancelEnter() {
573        setGhostVisibility(View.INVISIBLE);
574        mHasStopped = true;
575        mIsCanceled = true;
576        mResultReceiver = null;
577        if (mBackgroundAnimator != null) {
578            mBackgroundAnimator.cancel();
579            mBackgroundAnimator = null;
580        }
581        mActivity = null;
582        clearState();
583        return super.cancelPendingTransitions();
584    }
585
586    private void makeOpaque() {
587        if (!mHasStopped && mActivity != null) {
588            if (mWasOpaque) {
589                mActivity.convertFromTranslucent();
590            }
591            mActivity = null;
592        }
593    }
594
595    private boolean allowOverlappingTransitions() {
596        return mIsReturning ? getWindow().getAllowExitTransitionOverlap()
597                : getWindow().getAllowEnterTransitionOverlap();
598    }
599
600    private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) {
601        if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) {
602            return;
603        }
604        final ViewGroup decorView = getDecor();
605        if (decorView != null) {
606            ViewGroupOverlay overlay = decorView.getOverlay();
607            ObjectAnimator animator = null;
608            int numRejected = rejectedSnapshots.size();
609            for (int i = 0; i < numRejected; i++) {
610                View snapshot = rejectedSnapshots.get(i);
611                overlay.add(snapshot);
612                animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0);
613                animator.start();
614            }
615            animator.addListener(new AnimatorListenerAdapter() {
616                @Override
617                public void onAnimationEnd(Animator animation) {
618                    ViewGroupOverlay overlay = decorView.getOverlay();
619                    int numRejected = rejectedSnapshots.size();
620                    for (int i = 0; i < numRejected; i++) {
621                        overlay.remove(rejectedSnapshots.get(i));
622                    }
623                }
624            });
625        }
626    }
627
628    protected void onRemoteExitTransitionComplete() {
629        if (!allowOverlappingTransitions()) {
630            startEnterTransitionOnly();
631        }
632    }
633
634    private void startEnterTransitionOnly() {
635        startTransition(new Runnable() {
636            @Override
637            public void run() {
638                boolean startEnterTransition = true;
639                boolean startSharedElementTransition = false;
640                ViewGroup decorView = getDecor();
641                if (decorView != null) {
642                    Transition transition = beginTransition(decorView, startEnterTransition,
643                            startSharedElementTransition);
644                    startEnterTransition(transition);
645                }
646            }
647        });
648    }
649
650}
651