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            mSharedElementNames.clear();
252            mSharedElements.clear();
253            mAllSharedElementNames.clear();
254            startSharedElementTransition(null);
255            onRemoteExitTransitionComplete();
256        }
257    }
258
259    public boolean isReturning() {
260        return mIsReturning;
261    }
262
263    protected void prepareEnter() {
264        ViewGroup decorView = getDecor();
265        if (mActivity == null || decorView == null) {
266            return;
267        }
268        mActivity.overridePendingTransition(0, 0);
269        if (!mIsReturning) {
270            mWasOpaque = mActivity.convertToTranslucent(null, null);
271            Drawable background = decorView.getBackground();
272            if (background != null) {
273                getWindow().setBackgroundDrawable(null);
274                background = background.mutate();
275                background.setAlpha(0);
276                getWindow().setBackgroundDrawable(background);
277            }
278        } else {
279            mActivity = null; // all done with it now.
280        }
281    }
282
283    @Override
284    protected Transition getViewsTransition() {
285        Window window = getWindow();
286        if (window == null) {
287            return null;
288        }
289        if (mIsReturning) {
290            return window.getReenterTransition();
291        } else {
292            return window.getEnterTransition();
293        }
294    }
295
296    protected Transition getSharedElementTransition() {
297        Window window = getWindow();
298        if (window == null) {
299            return null;
300        }
301        if (mIsReturning) {
302            return window.getSharedElementReenterTransition();
303        } else {
304            return window.getSharedElementEnterTransition();
305        }
306    }
307
308    private void startSharedElementTransition(Bundle sharedElementState) {
309        ViewGroup decorView = getDecor();
310        if (decorView == null) {
311            return;
312        }
313        // Remove rejected shared elements
314        ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames);
315        rejectedNames.removeAll(mSharedElementNames);
316        ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames);
317        if (mListener != null) {
318            mListener.onRejectSharedElements(rejectedSnapshots);
319        }
320        removeNullViews(rejectedSnapshots);
321        startRejectedAnimations(rejectedSnapshots);
322
323        // Now start shared element transition
324        ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState,
325                mSharedElementNames);
326        showViews(mSharedElements, true);
327        scheduleSetSharedElementEnd(sharedElementSnapshots);
328        ArrayList<SharedElementOriginalState> originalImageViewState =
329                setSharedElementState(sharedElementState, sharedElementSnapshots);
330        requestLayoutForSharedElements();
331
332        boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning;
333        boolean startSharedElementTransition = true;
334        setGhostVisibility(View.INVISIBLE);
335        scheduleGhostVisibilityChange(View.INVISIBLE);
336        pauseInput();
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        OnSharedElementsReadyListener listener = new OnSharedElementsReadyListener() {
387            @Override
388            public void onSharedElementsReady() {
389                final View decorView = getDecor();
390                if (decorView != null) {
391                    decorView.getViewTreeObserver()
392                            .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
393                                @Override
394                                public boolean onPreDraw() {
395                                    decorView.getViewTreeObserver().removeOnPreDrawListener(this);
396                                    startTransition(new Runnable() {
397                                        @Override
398                                        public void run() {
399                                            startSharedElementTransition(sharedElementState);
400                                        }
401                                    });
402                                    return false;
403                                }
404                            });
405                    decorView.invalidate();
406                }
407            }
408        };
409        if (mListener == null) {
410            listener.onSharedElementsReady();
411        } else {
412            mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements, listener);
413        }
414    }
415
416    private void requestLayoutForSharedElements() {
417        int numSharedElements = mSharedElements.size();
418        for (int i = 0; i < numSharedElements; i++) {
419            mSharedElements.get(i).requestLayout();
420        }
421    }
422
423    private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition,
424            boolean startSharedElementTransition) {
425        Transition sharedElementTransition = null;
426        if (startSharedElementTransition) {
427            if (!mSharedElementNames.isEmpty()) {
428                sharedElementTransition = configureTransition(getSharedElementTransition(), false);
429            }
430            if (sharedElementTransition == null) {
431                sharedElementTransitionStarted();
432                sharedElementTransitionComplete();
433            } else {
434                sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() {
435                    @Override
436                    public void onTransitionStart(Transition transition) {
437                        sharedElementTransitionStarted();
438                    }
439
440                    @Override
441                    public void onTransitionEnd(Transition transition) {
442                        transition.removeListener(this);
443                        sharedElementTransitionComplete();
444                    }
445                });
446            }
447        }
448        Transition viewsTransition = null;
449        if (startEnterTransition) {
450            mIsViewsTransitionStarted = true;
451            if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
452                viewsTransition = configureTransition(getViewsTransition(), true);
453                if (viewsTransition != null && !mIsReturning) {
454                    stripOffscreenViews();
455                }
456            }
457            if (viewsTransition == null) {
458                viewsTransitionComplete();
459            } else {
460                viewsTransition.forceVisibility(View.INVISIBLE, true);
461                final ArrayList<View> transitioningViews = mTransitioningViews;
462                viewsTransition.addListener(new ContinueTransitionListener() {
463                    @Override
464                    public void onTransitionStart(Transition transition) {
465                        mEnterViewsTransition = transition;
466                        if (transitioningViews != null) {
467                            showViews(transitioningViews, false);
468                        }
469                        super.onTransitionStart(transition);
470                    }
471
472                    @Override
473                    public void onTransitionEnd(Transition transition) {
474                        mEnterViewsTransition = null;
475                        transition.removeListener(this);
476                        viewsTransitionComplete();
477                        super.onTransitionEnd(transition);
478                    }
479                });
480            }
481        }
482
483        Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
484        if (transition != null) {
485            transition.addListener(new ContinueTransitionListener());
486            TransitionManager.beginDelayedTransition(decorView, transition);
487            if (startSharedElementTransition && !mSharedElementNames.isEmpty()) {
488                mSharedElements.get(0).invalidate();
489            } else if (startEnterTransition && mTransitioningViews != null &&
490                    !mTransitioningViews.isEmpty()) {
491                mTransitioningViews.get(0).invalidate();
492            }
493        } else {
494            transitionStarted();
495        }
496        return transition;
497    }
498
499    @Override
500    protected void onTransitionsComplete() {
501        moveSharedElementsFromOverlay();
502    }
503
504    private void sharedElementTransitionStarted() {
505        mSharedElementTransitionStarted = true;
506        if (mIsExitTransitionComplete) {
507            send(MSG_EXIT_TRANSITION_COMPLETE, null);
508        }
509    }
510
511    private void startEnterTransition(Transition transition) {
512        ViewGroup decorView = getDecor();
513        if (!mIsReturning && decorView != null) {
514            Drawable background = decorView.getBackground();
515            if (background != null) {
516                background = background.mutate();
517                getWindow().setBackgroundDrawable(background);
518                mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255);
519                mBackgroundAnimator.setDuration(getFadeDuration());
520                mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
521                    @Override
522                    public void onAnimationEnd(Animator animation) {
523                        makeOpaque();
524                    }
525                });
526                mBackgroundAnimator.start();
527            } else if (transition != null) {
528                transition.addListener(new Transition.TransitionListenerAdapter() {
529                    @Override
530                    public void onTransitionEnd(Transition transition) {
531                        transition.removeListener(this);
532                        makeOpaque();
533                    }
534                });
535            } else {
536                makeOpaque();
537            }
538        }
539    }
540
541    public void stop() {
542        // Restore the background to its previous state since the
543        // Activity is stopping.
544        if (mBackgroundAnimator != null) {
545            mBackgroundAnimator.end();
546            mBackgroundAnimator = null;
547        } else if (mWasOpaque) {
548            ViewGroup decorView = getDecor();
549            if (decorView != null) {
550                Drawable drawable = decorView.getBackground();
551                if (drawable != null) {
552                    drawable.setAlpha(1);
553                }
554            }
555        }
556        makeOpaque();
557        mIsCanceled = true;
558        mResultReceiver = null;
559        mActivity = null;
560        moveSharedElementsFromOverlay();
561        if (mTransitioningViews != null) {
562            showViews(mTransitioningViews, true);
563        }
564        showViews(mSharedElements, true);
565        clearState();
566    }
567
568    /**
569     * Cancels the enter transition.
570     * @return True if the enter transition is still pending capturing the target state. If so,
571     * any transition started on the decor will do nothing.
572     */
573    public boolean cancelEnter() {
574        setGhostVisibility(View.INVISIBLE);
575        mHasStopped = true;
576        mIsCanceled = true;
577        mResultReceiver = null;
578        if (mBackgroundAnimator != null) {
579            mBackgroundAnimator.cancel();
580            mBackgroundAnimator = null;
581        }
582        mActivity = null;
583        clearState();
584        return super.cancelPendingTransitions();
585    }
586
587    private void makeOpaque() {
588        if (!mHasStopped && mActivity != null) {
589            if (mWasOpaque) {
590                mActivity.convertFromTranslucent();
591            }
592            mActivity = null;
593        }
594    }
595
596    private boolean allowOverlappingTransitions() {
597        return mIsReturning ? getWindow().getAllowReturnTransitionOverlap()
598                : getWindow().getAllowEnterTransitionOverlap();
599    }
600
601    private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) {
602        if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) {
603            return;
604        }
605        final ViewGroup decorView = getDecor();
606        if (decorView != null) {
607            ViewGroupOverlay overlay = decorView.getOverlay();
608            ObjectAnimator animator = null;
609            int numRejected = rejectedSnapshots.size();
610            for (int i = 0; i < numRejected; i++) {
611                View snapshot = rejectedSnapshots.get(i);
612                overlay.add(snapshot);
613                animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0);
614                animator.start();
615            }
616            animator.addListener(new AnimatorListenerAdapter() {
617                @Override
618                public void onAnimationEnd(Animator animation) {
619                    ViewGroupOverlay overlay = decorView.getOverlay();
620                    int numRejected = rejectedSnapshots.size();
621                    for (int i = 0; i < numRejected; i++) {
622                        overlay.remove(rejectedSnapshots.get(i));
623                    }
624                }
625            });
626        }
627    }
628
629    protected void onRemoteExitTransitionComplete() {
630        if (!allowOverlappingTransitions()) {
631            startEnterTransitionOnly();
632        }
633    }
634
635    private void startEnterTransitionOnly() {
636        startTransition(new Runnable() {
637            @Override
638            public void run() {
639                boolean startEnterTransition = true;
640                boolean startSharedElementTransition = false;
641                ViewGroup decorView = getDecor();
642                if (decorView != null) {
643                    Transition transition = beginTransition(decorView, startEnterTransition,
644                            startSharedElementTransition);
645                    startEnterTransition(transition);
646                }
647            }
648        });
649    }
650
651}
652