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.content.Intent;
23import android.graphics.Color;
24import android.graphics.Matrix;
25import android.graphics.RectF;
26import android.graphics.drawable.ColorDrawable;
27import android.graphics.drawable.Drawable;
28import android.os.Build.VERSION_CODES;
29import android.os.Bundle;
30import android.os.Handler;
31import android.os.Message;
32import android.os.ResultReceiver;
33import android.transition.Transition;
34import android.transition.TransitionManager;
35import android.view.View;
36import android.view.ViewGroup;
37import android.view.ViewTreeObserver;
38
39import java.util.ArrayList;
40
41/**
42 * This ActivityTransitionCoordinator is created in ActivityOptions#makeSceneTransitionAnimation
43 * to govern the exit of the Scene and the shared elements when calling an Activity as well as
44 * the reentry of the Scene when coming back from the called Activity.
45 */
46class ExitTransitionCoordinator extends ActivityTransitionCoordinator {
47    private static final String TAG = "ExitTransitionCoordinator";
48    private static final long MAX_WAIT_MS = 1000;
49
50    private Bundle mSharedElementBundle;
51
52    private boolean mExitNotified;
53
54    private boolean mSharedElementNotified;
55
56    private Activity mActivity;
57
58    private boolean mIsBackgroundReady;
59
60    private boolean mIsCanceled;
61
62    private Handler mHandler;
63
64    private ObjectAnimator mBackgroundAnimator;
65
66    private boolean mIsHidden;
67
68    private Bundle mExitSharedElementBundle;
69
70    private boolean mIsExitStarted;
71
72    private boolean mSharedElementsHidden;
73
74    public ExitTransitionCoordinator(Activity activity, ArrayList<String> names,
75            ArrayList<String> accepted, ArrayList<View> mapped, boolean isReturning) {
76        super(activity.getWindow(), names, getListener(activity, isReturning), isReturning);
77        viewsReady(mapSharedElements(accepted, mapped));
78        stripOffscreenViews();
79        mIsBackgroundReady = !isReturning;
80        mActivity = activity;
81    }
82
83    private static SharedElementCallback getListener(Activity activity, boolean isReturning) {
84        return isReturning ? activity.mEnterTransitionListener : activity.mExitTransitionListener;
85    }
86
87    @Override
88    protected void onReceiveResult(int resultCode, Bundle resultData) {
89        switch (resultCode) {
90            case MSG_SET_REMOTE_RECEIVER:
91                stopCancel();
92                mResultReceiver = resultData.getParcelable(KEY_REMOTE_RECEIVER);
93                if (mIsCanceled) {
94                    mResultReceiver.send(MSG_CANCEL, null);
95                    mResultReceiver = null;
96                } else {
97                    notifyComplete();
98                }
99                break;
100            case MSG_HIDE_SHARED_ELEMENTS:
101                stopCancel();
102                if (!mIsCanceled) {
103                    hideSharedElements();
104                }
105                break;
106            case MSG_START_EXIT_TRANSITION:
107                mHandler.removeMessages(MSG_CANCEL);
108                startExit();
109                break;
110            case MSG_SHARED_ELEMENT_DESTINATION:
111                mExitSharedElementBundle = resultData;
112                sharedElementExitBack();
113                break;
114        }
115    }
116
117    private void stopCancel() {
118        if (mHandler != null) {
119            mHandler.removeMessages(MSG_CANCEL);
120        }
121    }
122
123    private void delayCancel() {
124        if (mHandler != null) {
125            mHandler.sendEmptyMessageDelayed(MSG_CANCEL, MAX_WAIT_MS);
126        }
127    }
128
129    public void resetViews() {
130        if (mTransitioningViews != null) {
131            showViews(mTransitioningViews, true);
132        }
133        showViews(mSharedElements, true);
134        mIsHidden = true;
135        ViewGroup decorView = getDecor();
136        if (!mIsReturning && decorView != null) {
137            decorView.suppressLayout(false);
138        }
139        moveSharedElementsFromOverlay();
140        clearState();
141    }
142
143    private void sharedElementExitBack() {
144        final ViewGroup decorView = getDecor();
145        if (decorView != null) {
146            decorView.suppressLayout(true);
147        }
148        if (decorView != null && mExitSharedElementBundle != null &&
149                !mExitSharedElementBundle.isEmpty() &&
150                !mSharedElements.isEmpty() && getSharedElementTransition() != null) {
151            startTransition(new Runnable() {
152                public void run() {
153                    startSharedElementExit(decorView);
154                }
155            });
156        } else {
157            sharedElementTransitionComplete();
158        }
159    }
160
161    private void startSharedElementExit(final ViewGroup decorView) {
162        Transition transition = getSharedElementExitTransition();
163        transition.addListener(new Transition.TransitionListenerAdapter() {
164            @Override
165            public void onTransitionEnd(Transition transition) {
166                transition.removeListener(this);
167                if (isViewsTransitionComplete()) {
168                    delayCancel();
169                }
170            }
171        });
172        final ArrayList<View> sharedElementSnapshots = createSnapshots(mExitSharedElementBundle,
173                mSharedElementNames);
174        decorView.getViewTreeObserver()
175                .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
176                    @Override
177                    public boolean onPreDraw() {
178                        decorView.getViewTreeObserver().removeOnPreDrawListener(this);
179                        setSharedElementState(mExitSharedElementBundle, sharedElementSnapshots);
180                        return true;
181                    }
182                });
183        setGhostVisibility(View.INVISIBLE);
184        scheduleGhostVisibilityChange(View.INVISIBLE);
185        if (mListener != null) {
186            mListener.onSharedElementEnd(mSharedElementNames, mSharedElements,
187                    sharedElementSnapshots);
188        }
189        TransitionManager.beginDelayedTransition(decorView, transition);
190        scheduleGhostVisibilityChange(View.VISIBLE);
191        setGhostVisibility(View.VISIBLE);
192        decorView.invalidate();
193    }
194
195    private void hideSharedElements() {
196        moveSharedElementsFromOverlay();
197        if (!mIsHidden) {
198            hideViews(mSharedElements);
199        }
200        mSharedElementsHidden = true;
201        finishIfNecessary();
202    }
203
204    public void startExit() {
205        if (!mIsExitStarted) {
206            mIsExitStarted = true;
207            pauseInput();
208            ViewGroup decorView = getDecor();
209            if (decorView != null) {
210                decorView.suppressLayout(true);
211            }
212            moveSharedElementsToOverlay();
213            startTransition(new Runnable() {
214                @Override
215                public void run() {
216                    beginTransitions();
217                }
218            });
219        }
220    }
221
222    public void startExit(int resultCode, Intent data) {
223        if (!mIsExitStarted) {
224            mIsExitStarted = true;
225            pauseInput();
226            ViewGroup decorView = getDecor();
227            if (decorView != null) {
228                decorView.suppressLayout(true);
229            }
230            mHandler = new Handler() {
231                @Override
232                public void handleMessage(Message msg) {
233                    mIsCanceled = true;
234                    finish();
235                }
236            };
237            delayCancel();
238            moveSharedElementsToOverlay();
239            if (decorView != null && decorView.getBackground() == null) {
240                getWindow().setBackgroundDrawable(new ColorDrawable(Color.BLACK));
241            }
242            final boolean targetsM = decorView == null || decorView.getContext()
243                    .getApplicationInfo().targetSdkVersion >= VERSION_CODES.M;
244            ArrayList<String> sharedElementNames = targetsM ? mSharedElementNames :
245                    mAllSharedElementNames;
246            ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(mActivity, this,
247                    sharedElementNames, resultCode, data);
248            mActivity.convertToTranslucent(new Activity.TranslucentConversionListener() {
249                @Override
250                public void onTranslucentConversionComplete(boolean drawComplete) {
251                    if (!mIsCanceled) {
252                        fadeOutBackground();
253                    }
254                }
255            }, options);
256            startTransition(new Runnable() {
257                @Override
258                public void run() {
259                    startExitTransition();
260                }
261            });
262        }
263    }
264
265    public void stop() {
266        if (mIsReturning && mActivity != null) {
267            // Override the previous ActivityOptions. We don't want the
268            // activity to have options since we're essentially canceling the
269            // transition and finishing right now.
270            mActivity.convertToTranslucent(null, null);
271            finish();
272        }
273    }
274
275    private void startExitTransition() {
276        Transition transition = getExitTransition();
277        ViewGroup decorView = getDecor();
278        if (transition != null && decorView != null && mTransitioningViews != null) {
279            TransitionManager.beginDelayedTransition(decorView, transition);
280            mTransitioningViews.get(0).invalidate();
281        } else {
282            transitionStarted();
283        }
284    }
285
286    private void fadeOutBackground() {
287        if (mBackgroundAnimator == null) {
288            ViewGroup decor = getDecor();
289            Drawable background;
290            if (decor != null && (background = decor.getBackground()) != null) {
291                background = background.mutate();
292                getWindow().setBackgroundDrawable(background);
293                mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 0);
294                mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
295                    @Override
296                    public void onAnimationEnd(Animator animation) {
297                        mBackgroundAnimator = null;
298                        if (!mIsCanceled) {
299                            mIsBackgroundReady = true;
300                            notifyComplete();
301                        }
302                    }
303                });
304                mBackgroundAnimator.setDuration(getFadeDuration());
305                mBackgroundAnimator.start();
306            } else {
307                mIsBackgroundReady = true;
308            }
309        }
310    }
311
312    private Transition getExitTransition() {
313        Transition viewsTransition = null;
314        if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
315            viewsTransition = configureTransition(getViewsTransition(), true);
316        }
317        if (viewsTransition == null) {
318            viewsTransitionComplete();
319        } else {
320            final ArrayList<View> transitioningViews = mTransitioningViews;
321            viewsTransition.addListener(new ContinueTransitionListener() {
322                @Override
323                public void onTransitionEnd(Transition transition) {
324                    transition.removeListener(this);
325                    viewsTransitionComplete();
326                    if (mIsHidden && transitioningViews != null) {
327                        showViews(transitioningViews, true);
328                    }
329                    if (mSharedElementBundle != null) {
330                        delayCancel();
331                    }
332                    super.onTransitionEnd(transition);
333                }
334            });
335            viewsTransition.forceVisibility(View.INVISIBLE, false);
336        }
337        return viewsTransition;
338    }
339
340    private Transition getSharedElementExitTransition() {
341        Transition sharedElementTransition = null;
342        if (!mSharedElements.isEmpty()) {
343            sharedElementTransition = configureTransition(getSharedElementTransition(), false);
344        }
345        if (sharedElementTransition == null) {
346            sharedElementTransitionComplete();
347        } else {
348            sharedElementTransition.addListener(new ContinueTransitionListener() {
349                @Override
350                public void onTransitionEnd(Transition transition) {
351                    transition.removeListener(this);
352                    sharedElementTransitionComplete();
353                    if (mIsHidden) {
354                        showViews(mSharedElements, true);
355                    }
356                }
357            });
358            mSharedElements.get(0).invalidate();
359        }
360        return sharedElementTransition;
361    }
362
363    private void beginTransitions() {
364        Transition sharedElementTransition = getSharedElementExitTransition();
365        Transition viewsTransition = getExitTransition();
366
367        Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
368        ViewGroup decorView = getDecor();
369        if (transition != null && decorView != null) {
370            setGhostVisibility(View.INVISIBLE);
371            scheduleGhostVisibilityChange(View.INVISIBLE);
372            TransitionManager.beginDelayedTransition(decorView, transition);
373            scheduleGhostVisibilityChange(View.VISIBLE);
374            setGhostVisibility(View.VISIBLE);
375            decorView.invalidate();
376        } else {
377            transitionStarted();
378        }
379    }
380
381    protected boolean isReadyToNotify() {
382        return mSharedElementBundle != null && mResultReceiver != null && mIsBackgroundReady;
383    }
384
385    @Override
386    protected void sharedElementTransitionComplete() {
387        mSharedElementBundle = mExitSharedElementBundle == null
388                ? captureSharedElementState() : captureExitSharedElementsState();
389        super.sharedElementTransitionComplete();
390    }
391
392    private Bundle captureExitSharedElementsState() {
393        Bundle bundle = new Bundle();
394        RectF bounds = new RectF();
395        Matrix matrix = new Matrix();
396        for (int i = 0; i < mSharedElements.size(); i++) {
397            String name = mSharedElementNames.get(i);
398            Bundle sharedElementState = mExitSharedElementBundle.getBundle(name);
399            if (sharedElementState != null) {
400                bundle.putBundle(name, sharedElementState);
401            } else {
402                View view = mSharedElements.get(i);
403                captureSharedElementState(view, name, bundle, matrix, bounds);
404            }
405        }
406        return bundle;
407    }
408
409    @Override
410    protected void onTransitionsComplete() {
411        notifyComplete();
412    }
413
414    protected void notifyComplete() {
415        if (isReadyToNotify()) {
416            if (!mSharedElementNotified) {
417                mSharedElementNotified = true;
418                delayCancel();
419                if (mListener == null) {
420                    mResultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, mSharedElementBundle);
421                    notifyExitComplete();
422                } else {
423                    final ResultReceiver resultReceiver = mResultReceiver;
424                    final Bundle sharedElementBundle = mSharedElementBundle;
425                    mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements,
426                            new OnSharedElementsReadyListener() {
427                                @Override
428                                public void onSharedElementsReady() {
429                                    resultReceiver.send(MSG_TAKE_SHARED_ELEMENTS,
430                                            sharedElementBundle);
431                                    notifyExitComplete();
432                                }
433                            });
434                }
435            } else {
436                notifyExitComplete();
437            }
438        }
439    }
440
441    private void notifyExitComplete() {
442        if (!mExitNotified && isViewsTransitionComplete()) {
443            mExitNotified = true;
444            mResultReceiver.send(MSG_EXIT_TRANSITION_COMPLETE, null);
445            mResultReceiver = null; // done talking
446            ViewGroup decorView = getDecor();
447            if (!mIsReturning && decorView != null) {
448                decorView.suppressLayout(false);
449            }
450            finishIfNecessary();
451        }
452    }
453
454    private void finishIfNecessary() {
455        if (mIsReturning && mExitNotified && mActivity != null && (mSharedElements.isEmpty() ||
456                mSharedElementsHidden)) {
457            finish();
458        }
459        if (!mIsReturning && mExitNotified) {
460            mActivity = null; // don't need it anymore
461        }
462    }
463
464    private void finish() {
465        stopCancel();
466        if (mActivity != null) {
467            mActivity.mActivityTransitionState.clear();
468            mActivity.finish();
469            mActivity.overridePendingTransition(0, 0);
470            mActivity = null;
471        }
472        // Clear the state so that we can't hold any references accidentally and leak memory.
473        mHandler = null;
474        mSharedElementBundle = null;
475        if (mBackgroundAnimator != null) {
476            mBackgroundAnimator.cancel();
477            mBackgroundAnimator = null;
478        }
479        mExitSharedElementBundle = null;
480        clearState();
481    }
482
483    @Override
484    protected boolean moveSharedElementWithParent() {
485        return !mIsReturning;
486    }
487
488    @Override
489    protected Transition getViewsTransition() {
490        if (mIsReturning) {
491            return getWindow().getReturnTransition();
492        } else {
493            return getWindow().getExitTransition();
494        }
495    }
496
497    protected Transition getSharedElementTransition() {
498        if (mIsReturning) {
499            return getWindow().getSharedElementReturnTransition();
500        } else {
501            return getWindow().getSharedElementExitTransition();
502        }
503    }
504}
505