1 /*
2  * Copyright (C) 2013 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 
17 package android.transition;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ObjectAnimator;
23 import android.animation.PropertyValuesHolder;
24 import android.animation.RectEvaluator;
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.compat.annotation.UnsupportedAppUsage;
28 import android.content.Context;
29 import android.content.res.TypedArray;
30 import android.graphics.Bitmap;
31 import android.graphics.Canvas;
32 import android.graphics.Path;
33 import android.graphics.PointF;
34 import android.graphics.Rect;
35 import android.graphics.drawable.BitmapDrawable;
36 import android.graphics.drawable.Drawable;
37 import android.os.Build;
38 import android.util.AttributeSet;
39 import android.util.Property;
40 import android.view.View;
41 import android.view.ViewGroup;
42 
43 import com.android.internal.R;
44 
45 import java.util.Map;
46 
47 /**
48  * This transition captures the layout bounds of target views before and after
49  * the scene change and animates those changes during the transition.
50  *
51  * <p>A ChangeBounds transition can be described in a resource file by using the
52  * tag <code>changeBounds</code>, using its attributes of
53  * {@link android.R.styleable#ChangeBounds} along with the other standard
54  * attributes of {@link android.R.styleable#Transition}.</p>
55  */
56 public class ChangeBounds extends Transition {
57 
58     private static final String PROPNAME_BOUNDS = "android:changeBounds:bounds";
59     private static final String PROPNAME_CLIP = "android:changeBounds:clip";
60     private static final String PROPNAME_PARENT = "android:changeBounds:parent";
61     private static final String PROPNAME_WINDOW_X = "android:changeBounds:windowX";
62     private static final String PROPNAME_WINDOW_Y = "android:changeBounds:windowY";
63     private static final String[] sTransitionProperties = {
64             PROPNAME_BOUNDS,
65             PROPNAME_CLIP,
66             PROPNAME_PARENT,
67             PROPNAME_WINDOW_X,
68             PROPNAME_WINDOW_Y
69     };
70 
71     private static final Property<Drawable, PointF> DRAWABLE_ORIGIN_PROPERTY =
72             new Property<Drawable, PointF>(PointF.class, "boundsOrigin") {
73                 private Rect mBounds = new Rect();
74 
75                 @Override
76                 public void set(Drawable object, PointF value) {
77                     object.copyBounds(mBounds);
78                     mBounds.offsetTo(Math.round(value.x), Math.round(value.y));
79                     object.setBounds(mBounds);
80                 }
81 
82                 @Override
83                 public PointF get(Drawable object) {
84                     object.copyBounds(mBounds);
85                     return new PointF(mBounds.left, mBounds.top);
86                 }
87     };
88 
89     private static final Property<ViewBounds, PointF> TOP_LEFT_PROPERTY =
90             new Property<ViewBounds, PointF>(PointF.class, "topLeft") {
91                 @Override
92                 public void set(ViewBounds viewBounds, PointF topLeft) {
93                     viewBounds.setTopLeft(topLeft);
94                 }
95 
96                 @Override
97                 public PointF get(ViewBounds viewBounds) {
98                     return null;
99                 }
100             };
101 
102     private static final Property<ViewBounds, PointF> BOTTOM_RIGHT_PROPERTY =
103             new Property<ViewBounds, PointF>(PointF.class, "bottomRight") {
104                 @Override
105                 public void set(ViewBounds viewBounds, PointF bottomRight) {
106                     viewBounds.setBottomRight(bottomRight);
107                 }
108 
109                 @Override
110                 public PointF get(ViewBounds viewBounds) {
111                     return null;
112                 }
113             };
114 
115     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
116     private static final Property<View, PointF> BOTTOM_RIGHT_ONLY_PROPERTY =
117             new Property<View, PointF>(PointF.class, "bottomRight") {
118                 @Override
119                 public void set(View view, PointF bottomRight) {
120                     int left = view.getLeft();
121                     int top = view.getTop();
122                     int right = Math.round(bottomRight.x);
123                     int bottom = Math.round(bottomRight.y);
124                     view.setLeftTopRightBottom(left, top, right, bottom);
125                 }
126 
127                 @Override
128                 public PointF get(View view) {
129                     return null;
130                 }
131             };
132 
133     private static final Property<View, PointF> TOP_LEFT_ONLY_PROPERTY =
134             new Property<View, PointF>(PointF.class, "topLeft") {
135                 @Override
136                 public void set(View view, PointF topLeft) {
137                     int left = Math.round(topLeft.x);
138                     int top = Math.round(topLeft.y);
139                     int right = view.getRight();
140                     int bottom = view.getBottom();
141                     view.setLeftTopRightBottom(left, top, right, bottom);
142                 }
143 
144                 @Override
145                 public PointF get(View view) {
146                     return null;
147                 }
148             };
149 
150     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
151     private static final Property<View, PointF> POSITION_PROPERTY =
152             new Property<View, PointF>(PointF.class, "position") {
153                 @Override
154                 public void set(View view, PointF topLeft) {
155                     int left = Math.round(topLeft.x);
156                     int top = Math.round(topLeft.y);
157                     int right = left + view.getWidth();
158                     int bottom = top + view.getHeight();
159                     view.setLeftTopRightBottom(left, top, right, bottom);
160                 }
161 
162                 @Override
163                 public PointF get(View view) {
164                     return null;
165                 }
166             };
167 
168     int[] tempLocation = new int[2];
169     boolean mResizeClip = false;
170     boolean mReparent = false;
171     private static final String LOG_TAG = "ChangeBounds";
172 
173     private static RectEvaluator sRectEvaluator = new RectEvaluator();
174 
ChangeBounds()175     public ChangeBounds() {}
176 
ChangeBounds(Context context, AttributeSet attrs)177     public ChangeBounds(Context context, AttributeSet attrs) {
178         super(context, attrs);
179 
180         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ChangeBounds);
181         boolean resizeClip = a.getBoolean(R.styleable.ChangeBounds_resizeClip, false);
182         a.recycle();
183         setResizeClip(resizeClip);
184     }
185 
186     @Override
getTransitionProperties()187     public String[] getTransitionProperties() {
188         return sTransitionProperties;
189     }
190 
191     /**
192      * When <code>resizeClip</code> is true, ChangeBounds resizes the view using the clipBounds
193      * instead of changing the dimensions of the view during the animation. When
194      * <code>resizeClip</code> is false, ChangeBounds resizes the View by changing its dimensions.
195      *
196      * <p>When resizeClip is set to true, the clip bounds is modified by ChangeBounds. Therefore,
197      * {@link android.transition.ChangeClipBounds} is not compatible with ChangeBounds
198      * in this mode.</p>
199      *
200      * @param resizeClip Used to indicate whether the view bounds should be modified or the
201      *                   clip bounds should be modified by ChangeBounds.
202      * @see android.view.View#setClipBounds(android.graphics.Rect)
203      * @attr ref android.R.styleable#ChangeBounds_resizeClip
204      */
setResizeClip(boolean resizeClip)205     public void setResizeClip(boolean resizeClip) {
206         mResizeClip = resizeClip;
207     }
208 
209     /**
210      * Returns true when the ChangeBounds will resize by changing the clip bounds during the
211      * view animation or false when bounds are changed. The default value is false.
212      *
213      * @return true when the ChangeBounds will resize by changing the clip bounds during the
214      * view animation or false when bounds are changed. The default value is false.
215      * @attr ref android.R.styleable#ChangeBounds_resizeClip
216      */
getResizeClip()217     public boolean getResizeClip() {
218         return mResizeClip;
219     }
220 
221     /**
222      * Setting this flag tells ChangeBounds to track the before/after parent
223      * of every view using this transition. The flag is not enabled by
224      * default because it requires the parent instances to be the same
225      * in the two scenes or else all parents must use ids to allow
226      * the transition to determine which parents are the same.
227      *
228      * @param reparent true if the transition should track the parent
229      * container of target views and animate parent changes.
230      * @deprecated Use {@link android.transition.ChangeTransform} to handle
231      * transitions between different parents.
232      */
233     @Deprecated
setReparent(boolean reparent)234     public void setReparent(boolean reparent) {
235         mReparent = reparent;
236     }
237 
captureValues(TransitionValues values)238     private void captureValues(TransitionValues values) {
239         View view = values.view;
240 
241         if (view.isLaidOut() || view.getWidth() != 0 || view.getHeight() != 0) {
242             values.values.put(PROPNAME_BOUNDS, new Rect(view.getLeft(), view.getTop(),
243                     view.getRight(), view.getBottom()));
244             values.values.put(PROPNAME_PARENT, values.view.getParent());
245             if (mReparent) {
246                 values.view.getLocationInWindow(tempLocation);
247                 values.values.put(PROPNAME_WINDOW_X, tempLocation[0]);
248                 values.values.put(PROPNAME_WINDOW_Y, tempLocation[1]);
249             }
250             if (mResizeClip) {
251                 values.values.put(PROPNAME_CLIP, view.getClipBounds());
252             }
253         }
254     }
255 
256     @Override
captureStartValues(TransitionValues transitionValues)257     public void captureStartValues(TransitionValues transitionValues) {
258         captureValues(transitionValues);
259     }
260 
261     @Override
captureEndValues(TransitionValues transitionValues)262     public void captureEndValues(TransitionValues transitionValues) {
263         captureValues(transitionValues);
264     }
265 
parentMatches(View startParent, View endParent)266     private boolean parentMatches(View startParent, View endParent) {
267         boolean parentMatches = true;
268         if (mReparent) {
269             TransitionValues endValues = getMatchedTransitionValues(startParent, true);
270             if (endValues == null) {
271                 parentMatches = startParent == endParent;
272             } else {
273                 parentMatches = endParent == endValues.view;
274             }
275         }
276         return parentMatches;
277     }
278 
279     @Nullable
280     @Override
createAnimator(@onNull final ViewGroup sceneRoot, @Nullable TransitionValues startValues, @Nullable TransitionValues endValues)281     public Animator createAnimator(@NonNull final ViewGroup sceneRoot,
282             @Nullable TransitionValues startValues,
283             @Nullable TransitionValues endValues) {
284         if (startValues == null || endValues == null) {
285             return null;
286         }
287         Map<String, Object> startParentVals = startValues.values;
288         Map<String, Object> endParentVals = endValues.values;
289         ViewGroup startParent = (ViewGroup) startParentVals.get(PROPNAME_PARENT);
290         ViewGroup endParent = (ViewGroup) endParentVals.get(PROPNAME_PARENT);
291         if (startParent == null || endParent == null) {
292             return null;
293         }
294         final View view = endValues.view;
295         if (parentMatches(startParent, endParent)) {
296             Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS);
297             Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS);
298             final int startLeft = startBounds.left;
299             final int endLeft = endBounds.left;
300             final int startTop = startBounds.top;
301             final int endTop = endBounds.top;
302             final int startRight = startBounds.right;
303             final int endRight = endBounds.right;
304             final int startBottom = startBounds.bottom;
305             final int endBottom = endBounds.bottom;
306             final int startWidth = startRight - startLeft;
307             final int startHeight = startBottom - startTop;
308             final int endWidth = endRight - endLeft;
309             final int endHeight = endBottom - endTop;
310             Rect startClip = (Rect) startValues.values.get(PROPNAME_CLIP);
311             Rect endClip = (Rect) endValues.values.get(PROPNAME_CLIP);
312             int numChanges = 0;
313             if ((startWidth != 0 && startHeight != 0) || (endWidth != 0 && endHeight != 0)) {
314                 if (startLeft != endLeft || startTop != endTop) ++numChanges;
315                 if (startRight != endRight || startBottom != endBottom) ++numChanges;
316             }
317             if ((startClip != null && !startClip.equals(endClip)) ||
318                     (startClip == null && endClip != null)) {
319                 ++numChanges;
320             }
321             if (numChanges > 0) {
322                 if (view.getParent() instanceof ViewGroup) {
323                     final ViewGroup parent = (ViewGroup) view.getParent();
324                     parent.suppressLayout(true);
325                     TransitionListener transitionListener = new TransitionListenerAdapter() {
326                         boolean mCanceled = false;
327 
328                         @Override
329                         public void onTransitionCancel(Transition transition) {
330                             parent.suppressLayout(false);
331                             mCanceled = true;
332                         }
333 
334                         @Override
335                         public void onTransitionEnd(Transition transition) {
336                             if (!mCanceled) {
337                                 parent.suppressLayout(false);
338                             }
339                             transition.removeListener(this);
340                         }
341 
342                         @Override
343                         public void onTransitionPause(Transition transition) {
344                             parent.suppressLayout(false);
345                         }
346 
347                         @Override
348                         public void onTransitionResume(Transition transition) {
349                             parent.suppressLayout(true);
350                         }
351                     };
352                     addListener(transitionListener);
353                 }
354                 Animator anim;
355                 if (!mResizeClip) {
356                     view.setLeftTopRightBottom(startLeft, startTop, startRight, startBottom);
357                     if (numChanges == 2) {
358                         if (startWidth == endWidth && startHeight == endHeight) {
359                             Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft,
360                                     endTop);
361                             anim = ObjectAnimator.ofObject(view, POSITION_PROPERTY, null,
362                                     topLeftPath);
363                         } else {
364                             final ViewBounds viewBounds = new ViewBounds(view);
365                             Path topLeftPath = getPathMotion().getPath(startLeft, startTop,
366                                     endLeft, endTop);
367                             ObjectAnimator topLeftAnimator = ObjectAnimator
368                                     .ofObject(viewBounds, TOP_LEFT_PROPERTY, null, topLeftPath);
369 
370                             Path bottomRightPath = getPathMotion().getPath(startRight, startBottom,
371                                     endRight, endBottom);
372                             ObjectAnimator bottomRightAnimator = ObjectAnimator.ofObject(viewBounds,
373                                     BOTTOM_RIGHT_PROPERTY, null, bottomRightPath);
374                             AnimatorSet set = new AnimatorSet();
375                             set.playTogether(topLeftAnimator, bottomRightAnimator);
376                             anim = set;
377                             set.addListener(new AnimatorListenerAdapter() {
378                                 // We need a strong reference to viewBounds until the
379                                 // animator ends.
380                                 private ViewBounds mViewBounds = viewBounds;
381                             });
382                         }
383                     } else if (startLeft != endLeft || startTop != endTop) {
384                         Path topLeftPath = getPathMotion().getPath(startLeft, startTop,
385                                 endLeft, endTop);
386                         anim = ObjectAnimator.ofObject(view, TOP_LEFT_ONLY_PROPERTY, null,
387                                 topLeftPath);
388                     } else {
389                         Path bottomRight = getPathMotion().getPath(startRight, startBottom,
390                                 endRight, endBottom);
391                         anim = ObjectAnimator.ofObject(view, BOTTOM_RIGHT_ONLY_PROPERTY, null,
392                                 bottomRight);
393                     }
394                 } else {
395                     int maxWidth = Math.max(startWidth, endWidth);
396                     int maxHeight = Math.max(startHeight, endHeight);
397 
398                     view.setLeftTopRightBottom(startLeft, startTop, startLeft + maxWidth,
399                             startTop + maxHeight);
400 
401                     ObjectAnimator positionAnimator = null;
402                     if (startLeft != endLeft || startTop != endTop) {
403                         Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft,
404                                 endTop);
405                         positionAnimator = ObjectAnimator.ofObject(view, POSITION_PROPERTY, null,
406                                 topLeftPath);
407                     }
408                     final Rect finalClip = endClip;
409                     if (startClip == null) {
410                         startClip = new Rect(0, 0, startWidth, startHeight);
411                     }
412                     if (endClip == null) {
413                         endClip = new Rect(0, 0, endWidth, endHeight);
414                     }
415                     ObjectAnimator clipAnimator = null;
416                     if (!startClip.equals(endClip)) {
417                         view.setClipBounds(startClip);
418                         clipAnimator = ObjectAnimator.ofObject(view, "clipBounds", sRectEvaluator,
419                                 startClip, endClip);
420                         clipAnimator.addListener(new AnimatorListenerAdapter() {
421                             private boolean mIsCanceled;
422 
423                             @Override
424                             public void onAnimationCancel(Animator animation) {
425                                 mIsCanceled = true;
426                             }
427 
428                             @Override
429                             public void onAnimationEnd(Animator animation) {
430                                 if (!mIsCanceled) {
431                                     view.setClipBounds(finalClip);
432                                     view.setLeftTopRightBottom(endLeft, endTop, endRight,
433                                             endBottom);
434                                 }
435                             }
436                         });
437                     }
438                     anim = TransitionUtils.mergeAnimators(positionAnimator,
439                             clipAnimator);
440                 }
441                 return anim;
442             }
443         } else {
444             sceneRoot.getLocationInWindow(tempLocation);
445             int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X) - tempLocation[0];
446             int startY = (Integer) startValues.values.get(PROPNAME_WINDOW_Y) - tempLocation[1];
447             int endX = (Integer) endValues.values.get(PROPNAME_WINDOW_X) - tempLocation[0];
448             int endY = (Integer) endValues.values.get(PROPNAME_WINDOW_Y) - tempLocation[1];
449             // TODO: also handle size changes: check bounds and animate size changes
450             if (startX != endX || startY != endY) {
451                 final int width = view.getWidth();
452                 final int height = view.getHeight();
453                 Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
454                 Canvas canvas = new Canvas(bitmap);
455                 view.draw(canvas);
456                 final BitmapDrawable drawable = new BitmapDrawable(bitmap);
457                 drawable.setBounds(startX, startY, startX + width, startY + height);
458                 final float transitionAlpha = view.getTransitionAlpha();
459                 view.setTransitionAlpha(0);
460                 sceneRoot.getOverlay().add(drawable);
461                 Path topLeftPath = getPathMotion().getPath(startX, startY, endX, endY);
462                 PropertyValuesHolder origin = PropertyValuesHolder.ofObject(
463                         DRAWABLE_ORIGIN_PROPERTY, null, topLeftPath);
464                 ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, origin);
465                 anim.addListener(new AnimatorListenerAdapter() {
466                     @Override
467                     public void onAnimationEnd(Animator animation) {
468                         sceneRoot.getOverlay().remove(drawable);
469                         view.setTransitionAlpha(transitionAlpha);
470                     }
471                 });
472                 return anim;
473             }
474         }
475         return null;
476     }
477 
478     private static class ViewBounds {
479         private int mLeft;
480         private int mTop;
481         private int mRight;
482         private int mBottom;
483         private View mView;
484         private int mTopLeftCalls;
485         private int mBottomRightCalls;
486 
ViewBounds(View view)487         public ViewBounds(View view) {
488             mView = view;
489         }
490 
setTopLeft(PointF topLeft)491         public void setTopLeft(PointF topLeft) {
492             mLeft = Math.round(topLeft.x);
493             mTop = Math.round(topLeft.y);
494             mTopLeftCalls++;
495             if (mTopLeftCalls == mBottomRightCalls) {
496                 setLeftTopRightBottom();
497             }
498         }
499 
setBottomRight(PointF bottomRight)500         public void setBottomRight(PointF bottomRight) {
501             mRight = Math.round(bottomRight.x);
502             mBottom = Math.round(bottomRight.y);
503             mBottomRightCalls++;
504             if (mTopLeftCalls == mBottomRightCalls) {
505                 setLeftTopRightBottom();
506             }
507         }
508 
setLeftTopRightBottom()509         private void setLeftTopRightBottom() {
510             mView.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
511             mTopLeftCalls = 0;
512             mBottomRightCalls = 0;
513         }
514     }
515 }
516