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