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