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  */
16 package androidx.core.app;
17 
18 import android.content.Context;
19 import android.graphics.Bitmap;
20 import android.graphics.Canvas;
21 import android.graphics.Matrix;
22 import android.graphics.Rect;
23 import android.graphics.RectF;
24 import android.graphics.drawable.BitmapDrawable;
25 import android.graphics.drawable.Drawable;
26 import android.os.Bundle;
27 import android.os.Parcelable;
28 import android.view.View;
29 import android.widget.ImageView;
30 import android.widget.ImageView.ScaleType;
31 
32 import java.util.List;
33 import java.util.Map;
34 
35 /**
36  * Listener provided in
37  * {@link androidx.fragment.app.FragmentActivity#setEnterSharedElementCallback(SharedElementCallback)} and
38  * {@link androidx.fragment.app.FragmentActivity#setExitSharedElementCallback(SharedElementCallback)}
39  * to monitor the Activity transitions. The events can be used to customize Activity
40  * Transition behavior.
41  */
42 public abstract class SharedElementCallback {
43     private Matrix mTempMatrix;
44     private static final 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      */
onSharedElementStart(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots)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      */
onSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots)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      */
onRejectSharedElements(List<View> rejectedSharedElements)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      */
onMapSharedElements(List<String> names, Map<String, View> sharedElements)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      */
onCaptureSharedElementSnapshot(View sharedElement, Matrix viewToGlobalMatrix, RectF screenBounds)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      */
createDrawableBitmap(Drawable drawable)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     @SuppressWarnings("deprecation")
onCreateSnapshotView(Context context, Parcelable snapshot)257     public View onCreateSnapshotView(Context context, Parcelable snapshot) {
258         ImageView view = null;
259         if (snapshot instanceof Bundle) {
260             Bundle bundle = (Bundle) snapshot;
261             Bitmap bitmap = (Bitmap) bundle.getParcelable(BUNDLE_SNAPSHOT_BITMAP);
262             if (bitmap == null) {
263                 return null;
264             }
265             ImageView imageView = new ImageView(context);
266             view = imageView;
267             imageView.setImageBitmap(bitmap);
268             imageView.setScaleType(
269                     ScaleType.valueOf(bundle.getString(BUNDLE_SNAPSHOT_IMAGE_SCALETYPE)));
270             if (imageView.getScaleType() == ScaleType.MATRIX) {
271                 float[] values = bundle.getFloatArray(BUNDLE_SNAPSHOT_IMAGE_MATRIX);
272                 Matrix matrix = new Matrix();
273                 matrix.setValues(values);
274                 imageView.setImageMatrix(matrix);
275             }
276         } else if (snapshot instanceof Bitmap) {
277             Bitmap bitmap = (Bitmap) snapshot;
278             view = new ImageView(context);
279             view.setImageBitmap(bitmap);
280         }
281         return view;
282     }
283 
284     /**
285      * Called during an Activity Transition when the shared elements have arrived at the
286      * final location and are ready to be transferred. This method is called for both the
287      * source and destination Activities.
288      * <p>
289      * When the shared elements are ready to be transferred,
290      * {@link OnSharedElementsReadyListener#onSharedElementsReady()}
291      * must be called to trigger the transfer.
292      * <p>
293      * The default behavior is to trigger the transfer immediately.
294      *
295      * @param sharedElementNames The names of the shared elements that are being transferred..
296      * @param sharedElements The shared elements that are part of the View hierarchy.
297      * @param listener The listener to call when the shared elements are ready to be hidden
298      *                 in the source Activity or shown in the destination Activity.
299      */
onSharedElementsArrived(List<String> sharedElementNames, List<View> sharedElements, OnSharedElementsReadyListener listener)300     public void onSharedElementsArrived(List<String> sharedElementNames,
301             List<View> sharedElements, OnSharedElementsReadyListener listener) {
302         listener.onSharedElementsReady();
303     }
304 
305     /**
306      * Listener to be called after {@link
307      * SharedElementCallback#onSharedElementsArrived(List, List, OnSharedElementsReadyListener)}
308      * when the shared elements are ready to be hidden in the source Activity and shown in the
309      * destination Activity.
310      */
311     public interface OnSharedElementsReadyListener {
312 
313         /**
314          * Call this method during or after the OnSharedElementsReadyListener has been received
315          * in {@link SharedElementCallback#onSharedElementsArrived(List, List,
316          * OnSharedElementsReadyListener)} to indicate that the shared elements are ready to be
317          * hidden in the source and shown in the destination Activity.
318          */
onSharedElementsReady()319         void onSharedElementsReady();
320     }
321 }
322