ActivityTransitionCoordinator.java revision 8c2614ce4328640642d8e8be437859e0508a39b4
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.content.Context;
19import android.content.res.Resources;
20import android.graphics.Bitmap;
21import android.graphics.Canvas;
22import android.graphics.Matrix;
23import android.graphics.Rect;
24import android.graphics.drawable.BitmapDrawable;
25import android.os.Bundle;
26import android.os.Handler;
27import android.os.ResultReceiver;
28import android.transition.Transition;
29import android.transition.TransitionSet;
30import android.util.ArrayMap;
31import android.util.Pair;
32import android.view.View;
33import android.view.ViewGroup;
34import android.view.ViewTreeObserver;
35import android.view.Window;
36import android.widget.ImageView;
37
38import java.util.ArrayList;
39import java.util.Collection;
40
41/**
42 * Base class for ExitTransitionCoordinator and EnterTransitionCoordinator, classes
43 * that manage activity transitions and the communications coordinating them between
44 * Activities. The ExitTransitionCoordinator is created in the
45 * ActivityOptions#makeSceneTransitionAnimation. The EnterTransitionCoordinator
46 * is created by ActivityOptions#createEnterActivityTransition by Activity when the window is
47 * attached.
48 *
49 * Typical startActivity goes like this:
50 * 1) ExitTransitionCoordinator created with ActivityOptions#makeSceneTransitionAnimation
51 * 2) Activity#startActivity called and that calls startExit() through
52 *    ActivityOptions#dispatchStartExit
53 *    - Exit transition starts by setting transitioning Views to INVISIBLE
54 * 3) Launched Activity starts, creating an EnterTransitionCoordinator.
55 *    - The Window is made translucent
56 *    - The Window background alpha is set to 0
57 *    - The transitioning views are made INVISIBLE
58 *    - MSG_SET_LISTENER is sent back to the ExitTransitionCoordinator.
59 * 4) The shared element transition completes.
60 *    - MSG_TAKE_SHARED_ELEMENTS is sent to the EnterTransitionCoordinator
61 * 5) The MSG_TAKE_SHARED_ELEMENTS is received by the EnterTransitionCoordinator.
62 *    - Shared elements are made VISIBLE
63 *    - Shared elements positions and size are set to match the end state of the calling
64 *      Activity.
65 *    - The shared element transition is started
66 *    - If the window allows overlapping transitions, the views transition is started by setting
67 *      the entering Views to VISIBLE and the background alpha is animated to opaque.
68 *    - MSG_HIDE_SHARED_ELEMENTS is sent to the ExitTransitionCoordinator
69 * 6) MSG_HIDE_SHARED_ELEMENTS is received by the ExitTransitionCoordinator
70 *    - The shared elements are made INVISIBLE
71 * 7) The exit transition completes in the calling Activity.
72 *    - MSG_EXIT_TRANSITION_COMPLETE is sent to the EnterTransitionCoordinator.
73 * 8) The MSG_EXIT_TRANSITION_COMPLETE is received by the EnterTransitionCoordinator.
74 *    - If the window doesn't allow overlapping enter transitions, the enter transition is started
75 *      by setting entering views to VISIBLE and the background is animated to opaque.
76 * 9) The background opacity animation completes.
77 *    - The window is made opaque
78 * 10) The calling Activity gets an onStop() call
79 *    - onActivityStopped() is called and all exited Views are made VISIBLE.
80 *
81 * Typical finishAfterTransition goes like this:
82 * 1) finishAfterTransition() creates an ExitTransitionCoordinator and calls startExit()
83 *    - The Window start transitioning to Translucent with a new ActivityOptions.
84 *    - If no background exists, a black background is substituted
85 *    - The shared elements in the scene are matched against those shared elements
86 *      that were sent by comparing the names.
87 *    - The exit transition is started by setting Views to INVISIBLE.
88 * 2) The ActivityOptions is received by the Activity and an EnterTransitionCoordinator is created.
89 *    - All transitioning views are made VISIBLE to reverse what was done when onActivityStopped()
90 *      was called
91 * 3) The Window is made translucent and a callback is received
92 *    - The background alpha is animated to 0
93 * 4) The background alpha animation completes
94 * 5) The shared element transition completes
95 *    - After both 4 & 5 complete, MSG_TAKE_SHARED_ELEMENTS is sent to the
96 *      EnterTransitionCoordinator
97 * 6) MSG_TAKE_SHARED_ELEMENTS is received by EnterTransitionCoordinator
98 *    - Shared elements are made VISIBLE
99 *    - Shared elements positions and size are set to match the end state of the calling
100 *      Activity.
101 *    - The shared element transition is started
102 *    - If the window allows overlapping transitions, the views transition is started by setting
103 *      the entering Views to VISIBLE.
104 *    - MSG_HIDE_SHARED_ELEMENTS is sent to the ExitTransitionCoordinator
105 * 7) MSG_HIDE_SHARED_ELEMENTS is received by the ExitTransitionCoordinator
106 *    - The shared elements are made INVISIBLE
107 * 8) The exit transition completes in the finishing Activity.
108 *    - MSG_EXIT_TRANSITION_COMPLETE is sent to the EnterTransitionCoordinator.
109 *    - finish() is called on the exiting Activity
110 * 9) The MSG_EXIT_TRANSITION_COMPLETE is received by the EnterTransitionCoordinator.
111 *    - If the window doesn't allow overlapping enter transitions, the enter transition is started
112 *      by setting entering views to VISIBLE.
113 */
114abstract class ActivityTransitionCoordinator extends ResultReceiver {
115    private static final String TAG = "ActivityTransitionCoordinator";
116
117    /**
118     * For Activity transitions, the called Activity's listener to receive calls
119     * when transitions complete.
120     */
121    static final String KEY_REMOTE_RECEIVER = "android:remoteReceiver";
122
123    protected static final String KEY_SCREEN_X = "shared_element:screenX";
124    protected static final String KEY_SCREEN_Y = "shared_element:screenY";
125    protected static final String KEY_TRANSLATION_Z = "shared_element:translationZ";
126    protected static final String KEY_WIDTH = "shared_element:width";
127    protected static final String KEY_HEIGHT = "shared_element:height";
128    protected static final String KEY_BITMAP = "shared_element:bitmap";
129    protected static final String KEY_SCALE_TYPE = "shared_element:scaleType";
130    protected static final String KEY_IMAGE_MATRIX = "shared_element:imageMatrix";
131
132    // The background fade in/out duration. TODO: Enable tuning this.
133    public static final int FADE_BACKGROUND_DURATION_MS = 300;
134
135    protected static final ImageView.ScaleType[] SCALE_TYPE_VALUES = ImageView.ScaleType.values();
136
137    /**
138     * Sent by the exiting coordinator (either EnterTransitionCoordinator
139     * or ExitTransitionCoordinator) after the shared elements have
140     * become stationary (shared element transition completes). This tells
141     * the remote coordinator to take control of the shared elements and
142     * that animations may begin. The remote Activity won't start entering
143     * until this message is received, but may wait for
144     * MSG_EXIT_TRANSITION_COMPLETE if allowOverlappingTransitions() is true.
145     */
146    public static final int MSG_SET_REMOTE_RECEIVER = 100;
147
148    /**
149     * Sent by the entering coordinator to tell the exiting coordinator
150     * to hide its shared elements after it has started its shared
151     * element transition. This is temporary until the
152     * interlock of shared elements is figured out.
153     */
154    public static final int MSG_HIDE_SHARED_ELEMENTS = 101;
155
156    /**
157     * Sent by the exiting Activity in ActivityOptions#dispatchActivityStopped
158     * to leave the Activity in a good state after it has been hidden.
159     */
160    public static final int MSG_ACTIVITY_STOPPED = 102;
161
162    /**
163     * Sent by the exiting coordinator (either EnterTransitionCoordinator
164     * or ExitTransitionCoordinator) after the shared elements have
165     * become stationary (shared element transition completes). This tells
166     * the remote coordinator to take control of the shared elements and
167     * that animations may begin. The remote Activity won't start entering
168     * until this message is received, but may wait for
169     * MSG_EXIT_TRANSITION_COMPLETE if allowOverlappingTransitions() is true.
170     */
171    public static final int MSG_TAKE_SHARED_ELEMENTS = 103;
172
173    /**
174     * Sent by the exiting coordinator (either
175     * EnterTransitionCoordinator or ExitTransitionCoordinator) after
176     * the exiting Views have finished leaving the scene. This will
177     * be ignored if allowOverlappingTransitions() is true on the
178     * remote coordinator. If it is false, it will trigger the enter
179     * transition to start.
180     */
181    public static final int MSG_EXIT_TRANSITION_COMPLETE = 104;
182
183    /**
184     * Sent by Activity#startActivity to begin the exit transition.
185     */
186    public static final int MSG_START_EXIT_TRANSITION = 105;
187
188    /**
189     * It took too long for a message from the entering Activity, so we canceled the transition.
190     */
191    public static final int MSG_CANCEL = 106;
192
193    /**
194     * When returning, this is the destination location for the shared element.
195     */
196    public static final int MSG_SHARED_ELEMENT_DESTINATION = 107;
197
198    /**
199     * Send the shared element positions.
200     */
201    public static final int MSG_SEND_SHARED_ELEMENT_DESTINATION = 108;
202
203    final private Window mWindow;
204    final protected ArrayList<String> mAllSharedElementNames;
205    final protected ArrayList<View> mSharedElements = new ArrayList<View>();
206    final protected ArrayList<String> mSharedElementNames = new ArrayList<String>();
207    final protected ArrayList<View> mTransitioningViews = new ArrayList<View>();
208    final protected SharedElementListener mListener;
209    protected ResultReceiver mResultReceiver;
210    final private FixedEpicenterCallback mEpicenterCallback = new FixedEpicenterCallback();
211    final protected boolean mIsReturning;
212
213    public ActivityTransitionCoordinator(Window window,
214            ArrayList<String> allSharedElementNames,
215            ArrayList<String> accepted, ArrayList<String> localNames,
216            SharedElementListener listener, boolean isReturning) {
217        this(window, allSharedElementNames, listener, isReturning);
218        viewsReady(accepted, localNames);
219    }
220
221    public ActivityTransitionCoordinator(Window window,
222            ArrayList<String> allSharedElementNames,
223            SharedElementListener listener, boolean isReturning) {
224        super(new Handler());
225        mWindow = window;
226        mListener = listener;
227        mAllSharedElementNames = allSharedElementNames;
228        mIsReturning = isReturning;
229    }
230
231    protected void viewsReady(ArrayList<String> accepted, ArrayList<String> localNames) {
232        setSharedElements(accepted, localNames);
233        if (getViewsTransition() != null) {
234            getDecor().captureTransitioningViews(mTransitioningViews);
235            mTransitioningViews.removeAll(mSharedElements);
236        }
237        setEpicenter();
238    }
239
240    protected Window getWindow() {
241        return mWindow;
242    }
243
244    protected ViewGroup getDecor() {
245        return (mWindow == null) ? null : (ViewGroup) mWindow.getDecorView();
246    }
247
248    /**
249     * Sets the transition epicenter to the position of the first shared element.
250     */
251    protected void setEpicenter() {
252        View epicenter = null;
253        if (!mAllSharedElementNames.isEmpty() && !mSharedElementNames.isEmpty() &&
254                mAllSharedElementNames.get(0).equals(mSharedElementNames.get(0))) {
255            epicenter = mSharedElements.get(0);
256        }
257        setEpicenter(epicenter);
258    }
259
260    private void setEpicenter(View view) {
261        if (view == null) {
262            mEpicenterCallback.setEpicenter(null);
263        } else {
264            Rect epicenter = new Rect();
265            view.getBoundsOnScreen(epicenter);
266            mEpicenterCallback.setEpicenter(epicenter);
267        }
268    }
269
270    public ArrayList<String> getAcceptedNames() {
271        return mSharedElementNames;
272    }
273
274    public ArrayList<String> getMappedNames() {
275        ArrayList<String> names = new ArrayList<String>(mSharedElements.size());
276        for (int i = 0; i < mSharedElements.size(); i++) {
277            names.add(mSharedElements.get(i).getViewName());
278        }
279        return names;
280    }
281
282    public ArrayList<String> getAllSharedElementNames() { return mAllSharedElementNames; }
283
284    public static void setViewVisibility(Collection<View> views, int visibility) {
285        if (views != null) {
286            for (View view : views) {
287                view.setVisibility(visibility);
288            }
289        }
290    }
291
292    protected static Transition addTargets(Transition transition, Collection<View> views) {
293        if (transition == null || views == null || views.isEmpty()) {
294            return null;
295        }
296        TransitionSet set = new TransitionSet();
297        set.addTransition(transition);
298        if (views != null) {
299            for (View view: views) {
300                set.addTarget(view);
301            }
302        }
303        return set;
304    }
305
306    protected Transition configureTransition(Transition transition) {
307        if (transition != null) {
308            transition = transition.clone();
309            transition.setEpicenterCallback(mEpicenterCallback);
310        }
311        return transition;
312    }
313
314    protected static Transition mergeTransitions(Transition transition1, Transition transition2) {
315        if (transition1 == null) {
316            return transition2;
317        } else if (transition2 == null) {
318            return transition1;
319        } else {
320            TransitionSet transitionSet = new TransitionSet();
321            transitionSet.addTransition(transition1);
322            transitionSet.addTransition(transition2);
323            return transitionSet;
324        }
325    }
326
327    private void setSharedElements(ArrayList<String> accepted, ArrayList<String> localNames) {
328        if (!mAllSharedElementNames.isEmpty()) {
329            ArrayMap<String, View> sharedElements = new ArrayMap<String, View>();
330            getDecor().findNamedViews(sharedElements);
331            if (accepted != null) {
332                for (int i = 0; i < localNames.size(); i++) {
333                    String localName = localNames.get(i);
334                    String acceptedName = accepted.get(i);
335                    if (!localName.equals(acceptedName)) {
336                        View view = sharedElements.remove(localName);
337                        if (view != null) {
338                            sharedElements.put(acceptedName, view);
339                        }
340                    }
341                }
342            }
343            sharedElements.retainAll(mAllSharedElementNames);
344            mListener.remapSharedElements(mAllSharedElementNames, sharedElements);
345            sharedElements.retainAll(mAllSharedElementNames);
346            for (int i = 0; i < mAllSharedElementNames.size(); i++) {
347                String name = mAllSharedElementNames.get(i);
348                View sharedElement = sharedElements.get(name);
349                if (sharedElement != null) {
350                    mSharedElementNames.add(name);
351                    mSharedElements.add(sharedElement);
352                }
353            }
354        }
355    }
356
357    protected void setResultReceiver(ResultReceiver resultReceiver) {
358        mResultReceiver = resultReceiver;
359    }
360
361    protected abstract Transition getViewsTransition();
362
363    private static void setSharedElementState(View view, String name, Bundle transitionArgs,
364            int[] parentLoc) {
365        Bundle sharedElementBundle = transitionArgs.getBundle(name);
366        if (sharedElementBundle == null) {
367            return;
368        }
369
370        if (view instanceof ImageView) {
371            int scaleTypeInt = sharedElementBundle.getInt(KEY_SCALE_TYPE, -1);
372            if (scaleTypeInt >= 0) {
373                ImageView imageView = (ImageView) view;
374                ImageView.ScaleType scaleType = SCALE_TYPE_VALUES[scaleTypeInt];
375                imageView.setScaleType(scaleType);
376                if (scaleType == ImageView.ScaleType.MATRIX) {
377                    float[] matrixValues = sharedElementBundle.getFloatArray(KEY_IMAGE_MATRIX);
378                    Matrix matrix = new Matrix();
379                    matrix.setValues(matrixValues);
380                    imageView.setImageMatrix(matrix);
381                }
382            }
383        }
384
385        float z = sharedElementBundle.getFloat(KEY_TRANSLATION_Z);
386        view.setTranslationZ(z);
387
388        int x = sharedElementBundle.getInt(KEY_SCREEN_X);
389        int y = sharedElementBundle.getInt(KEY_SCREEN_Y);
390        int width = sharedElementBundle.getInt(KEY_WIDTH);
391        int height = sharedElementBundle.getInt(KEY_HEIGHT);
392
393        int widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
394        int heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
395        view.measure(widthSpec, heightSpec);
396
397        int left = x - parentLoc[0];
398        int top = y - parentLoc[1];
399        int right = left + width;
400        int bottom = top + height;
401        view.layout(left, top, right, bottom);
402    }
403
404    protected ArrayMap<ImageView, Pair<ImageView.ScaleType, Matrix>> setSharedElementState(
405            Bundle sharedElementState, final ArrayList<View> snapshots) {
406        ArrayMap<ImageView, Pair<ImageView.ScaleType, Matrix>> originalImageState =
407                new ArrayMap<ImageView, Pair<ImageView.ScaleType, Matrix>>();
408        if (sharedElementState != null) {
409            int[] tempLoc = new int[2];
410            for (int i = 0; i < mSharedElementNames.size(); i++) {
411                View sharedElement = mSharedElements.get(i);
412                String name = mSharedElementNames.get(i);
413                Pair<ImageView.ScaleType, Matrix> originalState = getOldImageState(sharedElement,
414                        name, sharedElementState);
415                if (originalState != null) {
416                    originalImageState.put((ImageView) sharedElement, originalState);
417                }
418                View parent = (View) sharedElement.getParent();
419                parent.getLocationOnScreen(tempLoc);
420                setSharedElementState(sharedElement, name, sharedElementState, tempLoc);
421            }
422        }
423        mListener.setSharedElementStart(mSharedElementNames, mSharedElements, snapshots);
424
425        getDecor().getViewTreeObserver().addOnPreDrawListener(
426                new ViewTreeObserver.OnPreDrawListener() {
427                    @Override
428                    public boolean onPreDraw() {
429                        getDecor().getViewTreeObserver().removeOnPreDrawListener(this);
430                        mListener.setSharedElementEnd(mSharedElementNames, mSharedElements,
431                                snapshots);
432                        return true;
433                    }
434                }
435        );
436        return originalImageState;
437    }
438
439    private static Pair<ImageView.ScaleType, Matrix> getOldImageState(View view, String name,
440            Bundle transitionArgs) {
441        if (!(view instanceof ImageView)) {
442            return null;
443        }
444        Bundle bundle = transitionArgs.getBundle(name);
445        if (bundle == null) {
446            return null;
447        }
448        int scaleTypeInt = bundle.getInt(KEY_SCALE_TYPE, -1);
449        if (scaleTypeInt < 0) {
450            return null;
451        }
452
453        ImageView imageView = (ImageView) view;
454        ImageView.ScaleType originalScaleType = imageView.getScaleType();
455
456        Matrix originalMatrix = null;
457        if (originalScaleType == ImageView.ScaleType.MATRIX) {
458            originalMatrix = new Matrix(imageView.getImageMatrix());
459        }
460
461        return Pair.create(originalScaleType, originalMatrix);
462    }
463
464    protected ArrayList<View> createSnapshots(Bundle state, Collection<String> names) {
465        int numSharedElements = names.size();
466        if (numSharedElements == 0) {
467            return null;
468        }
469        ArrayList<View> snapshots = new ArrayList<View>(numSharedElements);
470        Context context = getWindow().getContext();
471        int[] parentLoc = new int[2];
472        getDecor().getLocationOnScreen(parentLoc);
473        for (String name: names) {
474            Bundle sharedElementBundle = state.getBundle(name);
475            if (sharedElementBundle != null) {
476                Bitmap bitmap = sharedElementBundle.getParcelable(KEY_BITMAP);
477                View snapshot = new View(context);
478                Resources resources = getWindow().getContext().getResources();
479                if (bitmap != null) {
480                    snapshot.setBackground(new BitmapDrawable(resources, bitmap));
481                }
482                snapshot.setViewName(name);
483                setSharedElementState(snapshot, name, state, parentLoc);
484                snapshots.add(snapshot);
485            }
486        }
487        return snapshots;
488    }
489
490    protected static void setOriginalImageViewState(
491            ArrayMap<ImageView, Pair<ImageView.ScaleType, Matrix>> originalState) {
492        for (int i = 0; i < originalState.size(); i++) {
493            ImageView imageView = originalState.keyAt(i);
494            Pair<ImageView.ScaleType, Matrix> state = originalState.valueAt(i);
495            imageView.setScaleType(state.first);
496            imageView.setImageMatrix(state.second);
497        }
498    }
499
500    protected Bundle captureSharedElementState() {
501        Bundle bundle = new Bundle();
502        Rect tempBounds = new Rect();
503        for (int i = 0; i < mSharedElementNames.size(); i++) {
504            View sharedElement = mSharedElements.get(i);
505            String name = mSharedElementNames.get(i);
506            captureSharedElementState(sharedElement, name, bundle, tempBounds);
507        }
508        return bundle;
509    }
510
511    /**
512     * Captures placement information for Views with a shared element name for
513     * Activity Transitions.
514     *
515     * @param view           The View to capture the placement information for.
516     * @param name           The shared element name in the target Activity to apply the placement
517     *                       information for.
518     * @param transitionArgs Bundle to store shared element placement information.
519     * @param tempBounds     A temporary Rect for capturing the current location of views.
520     */
521    private static void captureSharedElementState(View view, String name, Bundle transitionArgs,
522            Rect tempBounds) {
523        Bundle sharedElementBundle = new Bundle();
524        tempBounds.set(0, 0, view.getWidth(), view.getHeight());
525        view.getBoundsOnScreen(tempBounds);
526        sharedElementBundle.putInt(KEY_SCREEN_X, tempBounds.left);
527        int width = tempBounds.width();
528        sharedElementBundle.putInt(KEY_WIDTH, width);
529
530        sharedElementBundle.putInt(KEY_SCREEN_Y, tempBounds.top);
531        int height = tempBounds.height();
532        sharedElementBundle.putInt(KEY_HEIGHT, height);
533
534        sharedElementBundle.putFloat(KEY_TRANSLATION_Z, view.getTranslationZ());
535
536        if (width > 0 && height > 0) {
537            Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
538            Canvas canvas = new Canvas(bitmap);
539            view.draw(canvas);
540            sharedElementBundle.putParcelable(KEY_BITMAP, bitmap);
541        }
542
543        if (view instanceof ImageView) {
544            ImageView imageView = (ImageView) view;
545            int scaleTypeInt = scaleTypeToInt(imageView.getScaleType());
546            sharedElementBundle.putInt(KEY_SCALE_TYPE, scaleTypeInt);
547            if (imageView.getScaleType() == ImageView.ScaleType.MATRIX) {
548                float[] matrix = new float[9];
549                imageView.getImageMatrix().getValues(matrix);
550                sharedElementBundle.putFloatArray(KEY_IMAGE_MATRIX, matrix);
551            }
552        }
553
554        transitionArgs.putBundle(name, sharedElementBundle);
555    }
556
557    private static int scaleTypeToInt(ImageView.ScaleType scaleType) {
558        for (int i = 0; i < SCALE_TYPE_VALUES.length; i++) {
559            if (scaleType == SCALE_TYPE_VALUES[i]) {
560                return i;
561            }
562        }
563        return -1;
564    }
565
566    private static class FixedEpicenterCallback extends Transition.EpicenterCallback {
567        private Rect mEpicenter;
568
569        public void setEpicenter(Rect epicenter) { mEpicenter = epicenter; }
570
571        @Override
572        public Rect onGetEpicenter(Transition transition) {
573            return mEpicenter;
574        }
575    }
576
577}
578