ExitTransitionCoordinator.java revision 80141d1c8b799cf7669a29b8837037cf3934ddb9
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 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 (isViewsTransitionComplete()) {
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            pauseInput();
207            ViewGroup decorView = getDecor();
208            if (decorView != null) {
209                decorView.suppressLayout(true);
210            }
211            moveSharedElementsToOverlay();
212            startTransition(new Runnable() {
213                @Override
214                public void run() {
215                    beginTransitions();
216                }
217            });
218        }
219    }
220
221    public void startExit(int resultCode, Intent data) {
222        if (!mIsExitStarted) {
223            mIsExitStarted = true;
224            pauseInput();
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                    mSharedElementNames, 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            viewsTransitionComplete();
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                    viewsTransitionComplete();
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    protected boolean isReadyToNotify() {
377        return mSharedElementBundle != null && mResultReceiver != null && mIsBackgroundReady;
378    }
379
380    @Override
381    protected void sharedElementTransitionComplete() {
382        mSharedElementBundle = mExitSharedElementBundle == null
383                ? captureSharedElementState() : captureExitSharedElementsState();
384        super.sharedElementTransitionComplete();
385    }
386
387    private Bundle captureExitSharedElementsState() {
388        Bundle bundle = new Bundle();
389        RectF bounds = new RectF();
390        Matrix matrix = new Matrix();
391        for (int i = 0; i < mSharedElements.size(); i++) {
392            String name = mSharedElementNames.get(i);
393            Bundle sharedElementState = mExitSharedElementBundle.getBundle(name);
394            if (sharedElementState != null) {
395                bundle.putBundle(name, sharedElementState);
396            } else {
397                View view = mSharedElements.get(i);
398                captureSharedElementState(view, name, bundle, matrix, bounds);
399            }
400        }
401        return bundle;
402    }
403
404    @Override
405    protected void onTransitionsComplete() {
406        notifyComplete();
407    }
408
409    protected void notifyComplete() {
410        if (isReadyToNotify()) {
411            if (!mSharedElementNotified) {
412                mSharedElementNotified = true;
413                delayCancel();
414                if (mListener == null) {
415                    mResultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, mSharedElementBundle);
416                    notifyExitComplete();
417                } else {
418                    final ResultReceiver resultReceiver = mResultReceiver;
419                    final Bundle sharedElementBundle = mSharedElementBundle;
420                    mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements,
421                            new OnSharedElementsReadyListener() {
422                                @Override
423                                public void onSharedElementsReady() {
424                                    resultReceiver.send(MSG_TAKE_SHARED_ELEMENTS,
425                                            sharedElementBundle);
426                                    notifyExitComplete();
427                                }
428                            });
429                }
430            } else {
431                notifyExitComplete();
432            }
433        }
434    }
435
436    private void notifyExitComplete() {
437        if (!mExitNotified && isViewsTransitionComplete()) {
438            mExitNotified = true;
439            mResultReceiver.send(MSG_EXIT_TRANSITION_COMPLETE, null);
440            mResultReceiver = null; // done talking
441            ViewGroup decorView = getDecor();
442            if (!mIsReturning && decorView != null) {
443                decorView.suppressLayout(false);
444            }
445            finishIfNecessary();
446        }
447    }
448
449    private void finishIfNecessary() {
450        if (mIsReturning && mExitNotified && mActivity != null && (mSharedElements.isEmpty() ||
451                mSharedElementsHidden)) {
452            finish();
453        }
454        if (!mIsReturning && mExitNotified) {
455            mActivity = null; // don't need it anymore
456        }
457    }
458
459    private void finish() {
460        stopCancel();
461        if (mActivity != null) {
462            mActivity.mActivityTransitionState.clear();
463            mActivity.finish();
464            mActivity.overridePendingTransition(0, 0);
465            mActivity = null;
466        }
467        // Clear the state so that we can't hold any references accidentally and leak memory.
468        mHandler = null;
469        mSharedElementBundle = null;
470        if (mBackgroundAnimator != null) {
471            mBackgroundAnimator.cancel();
472            mBackgroundAnimator = null;
473        }
474        mExitSharedElementBundle = null;
475        clearState();
476    }
477
478    @Override
479    protected boolean moveSharedElementWithParent() {
480        return !mIsReturning;
481    }
482
483    @Override
484    protected Transition getViewsTransition() {
485        if (mIsReturning) {
486            return getWindow().getReturnTransition();
487        } else {
488            return getWindow().getExitTransition();
489        }
490    }
491
492    protected Transition getSharedElementTransition() {
493        if (mIsReturning) {
494            return getWindow().getSharedElementReturnTransition();
495        } else {
496            return getWindow().getSharedElementExitTransition();
497        }
498    }
499}
500