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