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.support.v4.app;
17
18import android.content.Context;
19import android.graphics.Bitmap;
20import android.graphics.Canvas;
21import android.graphics.Matrix;
22import android.graphics.Rect;
23import android.graphics.RectF;
24import android.graphics.drawable.BitmapDrawable;
25import android.graphics.drawable.Drawable;
26import android.os.Bundle;
27import android.os.Parcelable;
28import android.view.View;
29import android.widget.ImageView;
30import android.widget.ImageView.ScaleType;
31
32import java.util.List;
33import java.util.Map;
34
35/**
36 * Listener provided in
37 * {@link FragmentActivity#setEnterSharedElementCallback(SharedElementCallback)} and
38 * {@link FragmentActivity#setExitSharedElementCallback(SharedElementCallback)}
39 * to monitor the Activity transitions. The events can be used to customize Activity
40 * Transition behavior.
41 */
42public abstract class SharedElementCallback {
43    private Matrix mTempMatrix;
44    private static int MAX_IMAGE_SIZE = (1024 * 1024);
45    private static final String BUNDLE_SNAPSHOT_BITMAP = "sharedElement:snapshot:bitmap";
46    private static final String BUNDLE_SNAPSHOT_IMAGE_SCALETYPE = "sharedElement:snapshot:imageScaleType";
47    private static final String BUNDLE_SNAPSHOT_IMAGE_MATRIX = "sharedElement:snapshot:imageMatrix";
48
49    /**
50     * In Activity Transitions, onSharedElementStart is called immediately before
51     * capturing the start of the shared element state on enter and reenter transitions and
52     * immediately before capturing the end of the shared element state for exit and return
53     * transitions.
54     * <p>
55     * In Fragment Transitions, onSharedElementStart is called immediately before capturing the
56     * start state of all shared element transitions.
57     * <p>
58     * This call can be used to adjust the transition start state by modifying the shared
59     * element Views. Note that no layout step will be executed between onSharedElementStart
60     * and the transition state capture.
61     * <p>
62     * For Activity Transitions, any changes made in {@link #onSharedElementEnd(List, List, List)}
63     * that are not updated during layout should be corrected in onSharedElementStart for exit and
64     * return transitions. For example, rotation or scale will not be affected by layout and
65     * if changed in {@link #onSharedElementEnd(List, List, List)}, it will also have to be reset
66     * in onSharedElementStart again to correct the end state.
67     *
68     * @param sharedElementNames The names of the shared elements that were accepted into
69     *                           the View hierarchy.
70     * @param sharedElements The shared elements that are part of the View hierarchy.
71     * @param sharedElementSnapshots The Views containing snap shots of the shared element
72     *                               from the launching Window. These elements will not
73     *                               be part of the scene, but will be positioned relative
74     *                               to the Window decor View. This list is null for Fragment
75     *                               Transitions.
76     */
77    public void onSharedElementStart(List<String> sharedElementNames,
78            List<View> sharedElements, List<View> sharedElementSnapshots) {}
79
80    /**
81     * In Activity Transitions, onSharedElementEnd is called immediately before
82     * capturing the end of the shared element state on enter and reenter transitions and
83     * immediately before capturing the start of the shared element state for exit and return
84     * transitions.
85     * <p>
86     * In Fragment Transitions, onSharedElementEnd is called immediately before capturing the
87     * end state of all shared element transitions.
88     * <p>
89     * This call can be used to adjust the transition end state by modifying the shared
90     * element Views. Note that no layout step will be executed between onSharedElementEnd
91     * and the transition state capture.
92     * <p>
93     * Any changes made in {@link #onSharedElementStart(List, List, List)} that are not updated
94     * during layout should be corrected in onSharedElementEnd. For example, rotation or scale
95     * will not be affected by layout and if changed in
96     * {@link #onSharedElementStart(List, List, List)}, it will also have to be reset in
97     * onSharedElementEnd again to correct the end state.
98     *
99     * @param sharedElementNames The names of the shared elements that were accepted into
100     *                           the View hierarchy.
101     * @param sharedElements The shared elements that are part of the View hierarchy.
102     * @param sharedElementSnapshots The Views containing snap shots of the shared element
103     *                               from the launching Window. These elements will not
104     *                               be part of the scene, but will be positioned relative
105     *                               to the Window decor View. This list will be null for
106     *                               Fragment Transitions.
107     */
108    public void onSharedElementEnd(List<String> sharedElementNames,
109            List<View> sharedElements, List<View> sharedElementSnapshots) {}
110
111    /**
112     * Called after {@link #onMapSharedElements(java.util.List, java.util.Map)} when
113     * transferring shared elements in. Any shared elements that have no mapping will be in
114     * <var>rejectedSharedElements</var>. The elements remaining in
115     * <var>rejectedSharedElements</var> will be transitioned out of the Scene. If a
116     * View is removed from <var>rejectedSharedElements</var>, it must be handled by the
117     * <code>SharedElementListener</code>.
118     * <p>
119     * Views in rejectedSharedElements will have their position and size set to the
120     * position of the calling shared element, relative to the Window decor View and contain
121     * snapshots of the View from the calling Activity or Fragment. This
122     * view may be safely added to the decor View's overlay to remain in position.
123     * </p>
124     * <p>This method is not called for Fragment Transitions. All rejected shared elements
125     * will be handled by the exit transition.</p>
126     *
127     * @param rejectedSharedElements Views containing visual information of shared elements
128     *                               that are not part of the entering scene. These Views
129     *                               are positioned relative to the Window decor View. A
130     *                               View removed from this list will not be transitioned
131     *                               automatically.
132     */
133    public void onRejectSharedElements(List<View> rejectedSharedElements) {}
134
135    /**
136     * Lets the SharedElementCallback adjust the mapping of shared element names to
137     * Views.
138     *
139     * @param names The names of all shared elements transferred from the calling Activity
140     *              or Fragment in the order they were provided.
141     * @param sharedElements The mapping of shared element names to Views. The best guess
142     *                       will be filled into sharedElements based on the transitionNames.
143     */
144    public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {}
145
146
147    /**
148     * Creates a snapshot of a shared element to be used by the remote Activity and reconstituted
149     * with {@link #onCreateSnapshotView(android.content.Context, android.os.Parcelable)}. A
150     * null return value will mean that the remote Activity will have a null snapshot View in
151     * {@link #onSharedElementStart(java.util.List, java.util.List, java.util.List)} and
152     * {@link #onSharedElementEnd(java.util.List, java.util.List, java.util.List)}.
153     *
154     * <p>This is not called for Fragment Transitions.</p>
155     *
156     * @param sharedElement The shared element View to create a snapshot for.
157     * @param viewToGlobalMatrix A matrix containing a transform from the view to the screen
158     *                           coordinates.
159     * @param screenBounds The bounds of shared element in screen coordinate space. This is
160     *                     the bounds of the view with the viewToGlobalMatrix applied.
161     * @return A snapshot to send to the remote Activity to be reconstituted with
162     * {@link #onCreateSnapshotView(android.content.Context, android.os.Parcelable)} and passed
163     * into {@link #onSharedElementStart(java.util.List, java.util.List, java.util.List)} and
164     * {@link #onSharedElementEnd(java.util.List, java.util.List, java.util.List)}.
165     */
166    public Parcelable onCaptureSharedElementSnapshot(View sharedElement, Matrix viewToGlobalMatrix,
167            RectF screenBounds) {
168        if (sharedElement instanceof ImageView) {
169            ImageView imageView = ((ImageView) sharedElement);
170            Drawable d = imageView.getDrawable();
171            Drawable bg = imageView.getBackground();
172            if (d != null && bg == null) {
173                Bitmap bitmap = createDrawableBitmap(d);
174                if (bitmap != null) {
175                    Bundle bundle = new Bundle();
176                    bundle.putParcelable(BUNDLE_SNAPSHOT_BITMAP, bitmap);
177                    bundle.putString(BUNDLE_SNAPSHOT_IMAGE_SCALETYPE,
178                            imageView.getScaleType().toString());
179                    if (imageView.getScaleType() == ScaleType.MATRIX) {
180                        Matrix matrix = imageView.getImageMatrix();
181                        float[] values = new float[9];
182                        matrix.getValues(values);
183                        bundle.putFloatArray(BUNDLE_SNAPSHOT_IMAGE_MATRIX, values);
184                    }
185                    return bundle;
186                }
187            }
188        }
189        int bitmapWidth = Math.round(screenBounds.width());
190        int bitmapHeight = Math.round(screenBounds.height());
191        Bitmap bitmap = null;
192        if (bitmapWidth > 0 && bitmapHeight > 0) {
193            float scale = Math.min(1f, ((float) MAX_IMAGE_SIZE) / (bitmapWidth * bitmapHeight));
194            bitmapWidth = (int) (bitmapWidth * scale);
195            bitmapHeight = (int) (bitmapHeight * scale);
196            if (mTempMatrix == null) {
197                mTempMatrix = new Matrix();
198            }
199            mTempMatrix.set(viewToGlobalMatrix);
200            mTempMatrix.postTranslate(-screenBounds.left, -screenBounds.top);
201            mTempMatrix.postScale(scale, scale);
202            bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
203            Canvas canvas = new Canvas(bitmap);
204            canvas.concat(mTempMatrix);
205            sharedElement.draw(canvas);
206        }
207        return bitmap;
208    }
209
210    /**
211     * Get a copy of bitmap of given drawable.
212     */
213    private static Bitmap createDrawableBitmap(Drawable drawable) {
214        int width = drawable.getIntrinsicWidth();
215        int height = drawable.getIntrinsicHeight();
216        if (width <= 0 || height <= 0) {
217            return null;
218        }
219        float scale = Math.min(1f, ((float)MAX_IMAGE_SIZE) / (width * height));
220        if (drawable instanceof BitmapDrawable && scale == 1f) {
221            // return same bitmap if scale down not needed
222            return ((BitmapDrawable) drawable).getBitmap();
223        }
224        int bitmapWidth = (int) (width * scale);
225        int bitmapHeight = (int) (height * scale);
226        Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
227        Canvas canvas = new Canvas(bitmap);
228        Rect existingBounds = drawable.getBounds();
229        int left = existingBounds.left;
230        int top = existingBounds.top;
231        int right = existingBounds.right;
232        int bottom = existingBounds.bottom;
233        drawable.setBounds(0, 0, bitmapWidth, bitmapHeight);
234        drawable.draw(canvas);
235        drawable.setBounds(left, top, right, bottom);
236        return bitmap;
237    }
238
239    /**
240     * Reconstitutes a snapshot View from a Parcelable returned in
241     * {@link #onCaptureSharedElementSnapshot(android.view.View, android.graphics.Matrix,
242     * android.graphics.RectF)} to be used in {@link #onSharedElementStart(java.util.List,
243     * java.util.List, java.util.List)} and {@link #onSharedElementEnd(java.util.List,
244     * java.util.List, java.util.List)}. The returned View will be sized and positioned after
245     * this call so that it is ready to be added to the decor View's overlay.
246     *
247     * <p>This is not called for Fragment Transitions.</p>
248     *
249     * @param context The Context used to create the snapshot View.
250     * @param snapshot The Parcelable returned by {@link #onCaptureSharedElementSnapshot(
251     * android.view.View, android.graphics.Matrix, android.graphics.RectF)}.
252     * @return A View to be sent in {@link #onSharedElementStart(java.util.List, java.util.List,
253     * java.util.List)} and {@link #onSharedElementEnd(java.util.List, java.util.List,
254     * java.util.List)}. A null value will produce a null snapshot value for those two methods.
255     */
256    public View onCreateSnapshotView(Context context, Parcelable snapshot) {
257        ImageView view = null;
258        if (snapshot instanceof Bundle) {
259            Bundle bundle = (Bundle) snapshot;
260            Bitmap bitmap = (Bitmap) bundle.getParcelable(BUNDLE_SNAPSHOT_BITMAP);
261            if (bitmap == null) {
262                return null;
263            }
264            ImageView imageView = new ImageView(context);
265            view = imageView;
266            imageView.setImageBitmap(bitmap);
267            imageView.setScaleType(
268                    ScaleType.valueOf(bundle.getString(BUNDLE_SNAPSHOT_IMAGE_SCALETYPE)));
269            if (imageView.getScaleType() == ScaleType.MATRIX) {
270                float[] values = bundle.getFloatArray(BUNDLE_SNAPSHOT_IMAGE_MATRIX);
271                Matrix matrix = new Matrix();
272                matrix.setValues(values);
273                imageView.setImageMatrix(matrix);
274            }
275        } else if (snapshot instanceof Bitmap) {
276            Bitmap bitmap = (Bitmap) snapshot;
277            view = new ImageView(context);
278            view.setImageBitmap(bitmap);
279        }
280        return view;
281    }
282
283    /**
284     * Called during an Activity Transition when the shared elements have arrived at the
285     * final location and are ready to be transferred. This method is called for both the
286     * source and destination Activities.
287     * <p>
288     * When the shared elements are ready to be transferred,
289     * {@link OnSharedElementsReadyListener#onSharedElementsReady()}
290     * must be called to trigger the transfer.
291     * <p>
292     * The default behavior is to trigger the transfer immediately.
293     *
294     * @param sharedElementNames The names of the shared elements that are being transferred..
295     * @param sharedElements The shared elements that are part of the View hierarchy.
296     * @param listener The listener to call when the shared elements are ready to be hidden
297     *                 in the source Activity or shown in the destination Activity.
298     */
299    public void onSharedElementsArrived(List<String> sharedElementNames,
300            List<View> sharedElements, OnSharedElementsReadyListener listener) {
301        listener.onSharedElementsReady();
302    }
303
304    /**
305     * Listener to be called after {@link
306     * SharedElementCallback#onSharedElementsArrived(List, List, OnSharedElementsReadyListener)}
307     * when the shared elements are ready to be hidden in the source Activity and shown in the
308     * destination Activity.
309     */
310    public interface OnSharedElementsReadyListener {
311
312        /**
313         * Call this method during or after the OnSharedElementsReadyListener has been received
314         * in {@link SharedElementCallback#onSharedElementsArrived(List, List,
315         * OnSharedElementsReadyListener)} to indicate that the shared elements are ready to be
316         * hidden in the source and shown in the destination Activity.
317         */
318        void onSharedElementsReady();
319    }
320}
321