EnterTransitionCoordinator.java revision 700db2a325bced35cebc403f272f988fad522892
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.graphics.Matrix;
22import android.graphics.drawable.Drawable;
23import android.os.Bundle;
24import android.os.Handler;
25import android.os.Message;
26import android.os.ResultReceiver;
27import android.transition.Transition;
28import android.transition.TransitionManager;
29import android.util.ArrayMap;
30import android.util.Pair;
31import android.view.View;
32import android.view.ViewGroup;
33import android.view.ViewGroupOverlay;
34import android.view.ViewTreeObserver;
35import android.widget.ImageView;
36
37import java.util.ArrayList;
38
39/**
40 * This ActivityTransitionCoordinator is created by the Activity to manage
41 * the enter scene and shared element transfer into the Scene, either during
42 * launch of an Activity or returning from a launched Activity.
43 */
44class EnterTransitionCoordinator extends ActivityTransitionCoordinator {
45    private static final String TAG = "EnterTransitionCoordinator";
46
47    private static final long MAX_WAIT_MS = 1000;
48    private static final int MIN_ANIMATION_FRAMES = 2;
49
50    private boolean mSharedElementTransitionStarted;
51    private Activity mActivity;
52    private boolean mHasStopped;
53    private Handler mHandler;
54    private boolean mIsCanceled;
55    private ObjectAnimator mBackgroundAnimator;
56    private boolean mIsExitTransitionComplete;
57    private boolean mIsReadyForTransition;
58    private Bundle mSharedElementsBundle;
59    private boolean mWasOpaque;
60    private boolean mAreViewsReady;
61
62    public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver,
63            ArrayList<String> sharedElementNames, boolean isReturning) {
64        super(activity.getWindow(), sharedElementNames,
65                getListener(activity, isReturning), isReturning);
66        mActivity = activity;
67        setResultReceiver(resultReceiver);
68        prepareEnter();
69        Bundle resultReceiverBundle = new Bundle();
70        resultReceiverBundle.putParcelable(KEY_REMOTE_RECEIVER, this);
71        mResultReceiver.send(MSG_SET_REMOTE_RECEIVER, resultReceiverBundle);
72        getDecor().getViewTreeObserver().addOnPreDrawListener(
73                new ViewTreeObserver.OnPreDrawListener() {
74                    @Override
75                    public boolean onPreDraw() {
76                        if (mIsReadyForTransition) {
77                            getDecor().getViewTreeObserver().removeOnPreDrawListener(this);
78                        }
79                        return mIsReadyForTransition;
80                    }
81                });
82    }
83
84    public void viewInstancesReady(ArrayList<String> accepted, ArrayList<View> localViews) {
85        triggerViewsReady(mapSharedElements(accepted, localViews));
86    }
87
88    public void namedViewsReady(ArrayList<String> accepted, ArrayList<String> localNames) {
89        triggerViewsReady(mapNamedElements(accepted, localNames));
90    }
91
92    @Override
93    protected void viewsReady(ArrayMap<String, View> sharedElements) {
94        super.viewsReady(sharedElements);
95        mIsReadyForTransition = true;
96        if (mIsReturning) {
97            mHandler = new Handler() {
98                @Override
99                public void handleMessage(Message msg) {
100                    cancel();
101                }
102            };
103            mHandler.sendEmptyMessageDelayed(MSG_CANCEL, MAX_WAIT_MS);
104            send(MSG_SEND_SHARED_ELEMENT_DESTINATION, null);
105        }
106        setViewVisibility(mSharedElements, View.INVISIBLE);
107        if (getViewsTransition() != null) {
108            setViewVisibility(mTransitioningViews, View.INVISIBLE);
109        }
110        if (mSharedElementsBundle != null) {
111            onTakeSharedElements();
112        }
113    }
114
115    private void triggerViewsReady(final ArrayMap<String, View> sharedElements) {
116        if (mAreViewsReady) {
117            return;
118        }
119        mAreViewsReady = true;
120        // Ensure the views have been laid out before capturing the views -- we need the epicenter.
121        if (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()) {
122            viewsReady(sharedElements);
123        } else {
124            sharedElements.valueAt(0).getViewTreeObserver()
125                    .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
126                @Override
127                public boolean onPreDraw() {
128                    sharedElements.valueAt(0).getViewTreeObserver().removeOnPreDrawListener(this);
129                    viewsReady(sharedElements);
130                    return true;
131                }
132            });
133        }
134    }
135
136    private ArrayMap<String, View> mapNamedElements(ArrayList<String> accepted,
137            ArrayList<String> localNames) {
138        ArrayMap<String, View> sharedElements = new ArrayMap<String, View>();
139        getDecor().findNamedViews(sharedElements);
140        if (accepted != null) {
141            for (int i = 0; i < localNames.size(); i++) {
142                String localName = localNames.get(i);
143                String acceptedName = accepted.get(i);
144                if (localName != null && !localName.equals(acceptedName)) {
145                    View view = sharedElements.remove(localName);
146                    if (view != null) {
147                        sharedElements.put(acceptedName, view);
148                    }
149                }
150            }
151        }
152        return sharedElements;
153    }
154
155    private void sendSharedElementDestination() {
156        ViewGroup decor = getDecor();
157        boolean allReady = !decor.isLayoutRequested();
158        if (allReady) {
159            for (int i = 0; i < mSharedElements.size(); i++) {
160                if (mSharedElements.get(i).isLayoutRequested()) {
161                    allReady = false;
162                    break;
163                }
164            }
165        }
166        if (allReady) {
167            Bundle state = captureSharedElementState();
168            mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state);
169        } else {
170            getDecor().getViewTreeObserver()
171                    .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
172                        @Override
173                        public boolean onPreDraw() {
174                            getDecor().getViewTreeObserver().removeOnPreDrawListener(this);
175                            Bundle state = captureSharedElementState();
176                            mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state);
177                            return true;
178                        }
179                    });
180        }
181        if (allowOverlappingTransitions()) {
182            startEnterTransitionOnly();
183        }
184    }
185
186    private static SharedElementListener getListener(Activity activity, boolean isReturning) {
187        return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener;
188    }
189
190    @Override
191    protected void onReceiveResult(int resultCode, Bundle resultData) {
192        switch (resultCode) {
193            case MSG_TAKE_SHARED_ELEMENTS:
194                if (!mIsCanceled) {
195                    if (mHandler != null) {
196                        mHandler.removeMessages(MSG_CANCEL);
197                    }
198                    mSharedElementsBundle = resultData;
199                    onTakeSharedElements();
200                }
201                break;
202            case MSG_EXIT_TRANSITION_COMPLETE:
203                if (!mIsCanceled) {
204                    mIsExitTransitionComplete = true;
205                    if (mSharedElementTransitionStarted) {
206                        onRemoteExitTransitionComplete();
207                    }
208                }
209                break;
210            case MSG_CANCEL:
211                cancel();
212                break;
213            case MSG_SEND_SHARED_ELEMENT_DESTINATION:
214                sendSharedElementDestination();
215                break;
216        }
217    }
218
219    private void cancel() {
220        if (!mIsCanceled) {
221            mIsCanceled = true;
222            if (getViewsTransition() == null) {
223                setViewVisibility(mSharedElements, View.VISIBLE);
224            } else {
225                mTransitioningViews.addAll(mSharedElements);
226            }
227            mSharedElementNames.clear();
228            mSharedElements.clear();
229            mAllSharedElementNames.clear();
230            startSharedElementTransition(null);
231            onRemoteExitTransitionComplete();
232        }
233    }
234
235    public boolean isReturning() {
236        return mIsReturning;
237    }
238
239    protected void prepareEnter() {
240        mActivity.overridePendingTransition(0, 0);
241        if (!mIsReturning) {
242            mWasOpaque = mActivity.convertToTranslucent(null, null);
243            Drawable background = getDecor().getBackground();
244            if (background != null) {
245                getWindow().setBackgroundDrawable(null);
246                background = background.mutate();
247                background.setAlpha(0);
248                getWindow().setBackgroundDrawable(background);
249            }
250        } else {
251            mActivity = null; // all done with it now.
252        }
253    }
254
255    @Override
256    protected Transition getViewsTransition() {
257        if (mIsReturning) {
258            return getWindow().getExitTransition();
259        } else {
260            return getWindow().getEnterTransition();
261        }
262    }
263
264    protected Transition getSharedElementTransition() {
265        if (mIsReturning) {
266            return getWindow().getSharedElementExitTransition();
267        } else {
268            return getWindow().getSharedElementEnterTransition();
269        }
270    }
271
272    private void startSharedElementTransition(Bundle sharedElementState) {
273        // Remove rejected shared elements
274        ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames);
275        rejectedNames.removeAll(mSharedElementNames);
276        ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames);
277        mListener.handleRejectedSharedElements(rejectedSnapshots);
278        startRejectedAnimations(rejectedSnapshots);
279
280        // Now start shared element transition
281        ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState,
282                mSharedElementNames);
283        setViewVisibility(mSharedElements, View.VISIBLE);
284        ArrayMap<ImageView, Pair<ImageView.ScaleType, Matrix>> originalImageViewState =
285                setSharedElementState(sharedElementState, sharedElementSnapshots);
286        requestLayoutForSharedElements();
287
288        boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning;
289        boolean startSharedElementTransition = true;
290        Transition transition = beginTransition(startEnterTransition, startSharedElementTransition);
291
292        if (startEnterTransition) {
293            startEnterTransition(transition);
294        }
295
296        setOriginalImageViewState(originalImageViewState);
297
298        if (mResultReceiver != null) {
299            // We can't trust that the view will disappear on the same frame that the shared
300            // element appears here. Assure that we get at least 2 frames for double-buffering.
301            getDecor().postOnAnimation(new Runnable() {
302                int mAnimations;
303                @Override
304                public void run() {
305                    if (mAnimations++ < MIN_ANIMATION_FRAMES) {
306                        getDecor().postOnAnimation(this);
307                    } else {
308                        mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null);
309                        mResultReceiver = null; // all done sending messages.
310                    }
311                }
312            });
313        }
314    }
315
316    @Override
317    protected void stripOffscreenViews() {
318        setViewVisibility(mTransitioningViews, View.VISIBLE);
319        super.stripOffscreenViews();
320        setViewVisibility(mTransitioningViews, View.INVISIBLE);
321    }
322
323    private void onTakeSharedElements() {
324        if (!mIsReadyForTransition || mSharedElementsBundle == null) {
325            return;
326        }
327        final Bundle sharedElementState = mSharedElementsBundle;
328        mSharedElementsBundle = null;
329        getDecor().getViewTreeObserver()
330                .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
331                    @Override
332                    public boolean onPreDraw() {
333                        getDecor().getViewTreeObserver().removeOnPreDrawListener(this);
334                        startTransition(new Runnable() {
335                            @Override
336                            public void run() {
337                                startSharedElementTransition(sharedElementState);
338                            }
339                        });
340                        return false;
341                    }
342                });
343        getDecor().invalidate();
344    }
345
346    private void requestLayoutForSharedElements() {
347        int numSharedElements = mSharedElements.size();
348        for (int i = 0; i < numSharedElements; i++) {
349            mSharedElements.get(i).requestLayout();
350        }
351    }
352
353    private Transition beginTransition(boolean startEnterTransition,
354            boolean startSharedElementTransition) {
355        Transition sharedElementTransition = null;
356        if (startSharedElementTransition && !mSharedElementNames.isEmpty()) {
357            sharedElementTransition = configureTransition(getSharedElementTransition(), false);
358        }
359        Transition viewsTransition = null;
360        if (startEnterTransition && !mTransitioningViews.isEmpty()) {
361            viewsTransition = configureTransition(getViewsTransition(), true);
362            if (viewsTransition != null) {
363                stripOffscreenViews();
364            }
365        }
366
367        Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
368        if (startSharedElementTransition) {
369            if (transition == null) {
370                sharedElementTransitionStarted();
371            } else {
372                transition.addListener(new ContinueTransitionListener() {
373                    @Override
374                    public void onTransitionStart(Transition transition) {
375                        super.onTransitionStart(transition);
376                        transition.removeListener(this);
377                        sharedElementTransitionStarted();
378                    }
379                });
380            }
381        }
382        if (transition != null) {
383            if (sharedElementTransition == null) {
384                transition.addListener(new ContinueTransitionListener());
385            }
386            TransitionManager.beginDelayedTransition(getDecor(), transition);
387            if (startSharedElementTransition && !mSharedElementNames.isEmpty()) {
388                mSharedElements.get(0).invalidate();
389            } else if (startEnterTransition && !mTransitioningViews.isEmpty()) {
390                mTransitioningViews.get(0).invalidate();
391            }
392        } else {
393            transitionStarted();
394        }
395        return transition;
396    }
397
398    private void sharedElementTransitionStarted() {
399        mSharedElementTransitionStarted = true;
400        if (mIsExitTransitionComplete) {
401            send(MSG_EXIT_TRANSITION_COMPLETE, null);
402        }
403    }
404
405    private void startEnterTransition(Transition transition) {
406        setViewVisibility(mTransitioningViews, View.VISIBLE);
407        if (!mIsReturning) {
408            Drawable background = getDecor().getBackground();
409            if (background != null) {
410                background = background.mutate();
411                mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255);
412                mBackgroundAnimator.setDuration(getFadeDuration());
413                mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
414                    @Override
415                    public void onAnimationEnd(Animator animation) {
416                        makeOpaque();
417                    }
418                });
419                mBackgroundAnimator.start();
420            } else if (transition != null) {
421                transition.addListener(new Transition.TransitionListenerAdapter() {
422                    @Override
423                    public void onTransitionEnd(Transition transition) {
424                        transition.removeListener(this);
425                        makeOpaque();
426                    }
427                });
428            } else {
429                makeOpaque();
430            }
431        }
432    }
433
434    public void stop() {
435        makeOpaque();
436        mIsCanceled = true;
437        mResultReceiver = null;
438        if (mBackgroundAnimator != null) {
439            mBackgroundAnimator.end();
440            mBackgroundAnimator = null;
441        }
442        mActivity = null;
443        clearState();
444    }
445
446    public void cancelEnter() {
447        mHasStopped = true;
448        mIsCanceled = true;
449        mResultReceiver = null;
450        if (mBackgroundAnimator != null) {
451            mBackgroundAnimator.cancel();
452            mBackgroundAnimator = null;
453        }
454        mActivity = null;
455        clearState();
456    }
457
458    private void makeOpaque() {
459        if (!mHasStopped && mActivity != null) {
460            if (mWasOpaque) {
461                mActivity.convertFromTranslucent();
462            }
463            mActivity = null;
464        }
465    }
466
467    private boolean allowOverlappingTransitions() {
468        return mIsReturning ? getWindow().getAllowExitTransitionOverlap()
469                : getWindow().getAllowEnterTransitionOverlap();
470    }
471
472    private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) {
473        if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) {
474            return;
475        }
476        ViewGroupOverlay overlay = getDecor().getOverlay();
477        ObjectAnimator animator = null;
478        int numRejected = rejectedSnapshots.size();
479        for (int i = 0; i < numRejected; i++) {
480            View snapshot = rejectedSnapshots.get(i);
481            overlay.add(snapshot);
482            animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0);
483            animator.start();
484        }
485        animator.addListener(new AnimatorListenerAdapter() {
486            @Override
487            public void onAnimationEnd(Animator animation) {
488                ViewGroupOverlay overlay = getDecor().getOverlay();
489                int numRejected = rejectedSnapshots.size();
490                for (int i = 0; i < numRejected; i++) {
491                    overlay.remove(rejectedSnapshots.get(i));
492                }
493            }
494        });
495    }
496
497    protected void onRemoteExitTransitionComplete() {
498        if (!allowOverlappingTransitions()) {
499            startEnterTransitionOnly();
500        }
501    }
502
503    private void startEnterTransitionOnly() {
504        startTransition(new Runnable() {
505            @Override
506            public void run() {
507                boolean startEnterTransition = true;
508                boolean startSharedElementTransition = false;
509                Transition transition = beginTransition(startEnterTransition,
510                        startSharedElementTransition);
511                startEnterTransition(transition);
512            }
513        });
514    }
515}
516