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