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