• 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.AnimatorSet;
20 import android.content.Context;
21 import android.content.res.TypedArray;
22 import android.graphics.PointF;
23 
24 import android.animation.Animator;
25 import android.animation.AnimatorListenerAdapter;
26 import android.animation.ObjectAnimator;
27 import android.animation.PropertyValuesHolder;
28 import android.animation.RectEvaluator;
29 import android.graphics.Bitmap;
30 import android.graphics.Canvas;
31 import android.graphics.Path;
32 import android.graphics.Rect;
33 import android.graphics.drawable.BitmapDrawable;
34 import android.graphics.drawable.Drawable;
35 import android.util.AttributeSet;
36 import android.util.Property;
37 import android.view.View;
38 import android.view.ViewGroup;
39 
40 import com.android.internal.R;
41 
42 import java.util.Map;
43 
44 /**
45  * This transition captures the layout bounds of target views before and after
46  * the scene change and animates those changes during the transition.
47  *
48  * <p>A ChangeBounds transition can be described in a resource file by using the
49  * tag <code>changeBounds</code>, using its attributes of
50  * {@link android.R.styleable#ChangeBounds} along with the other standard
51  * attributes of {@link android.R.styleable#Transition}.</p>
52  */
53 public class ChangeBounds extends Transition {
54 
55     private static final String PROPNAME_BOUNDS = "android:changeBounds:bounds";
56     private static final String PROPNAME_CLIP = "android:changeBounds:clip";
57     private static final String PROPNAME_PARENT = "android:changeBounds:parent";
58     private static final String PROPNAME_WINDOW_X = "android:changeBounds:windowX";
59     private static final String PROPNAME_WINDOW_Y = "android:changeBounds:windowY";
60     private static final String[] sTransitionProperties = {
61             PROPNAME_BOUNDS,
62             PROPNAME_CLIP,
63             PROPNAME_PARENT,
64             PROPNAME_WINDOW_X,
65             PROPNAME_WINDOW_Y
66     };
67 
68     private static final Property<Drawable, PointF> DRAWABLE_ORIGIN_PROPERTY =
69             new Property<Drawable, PointF>(PointF.class, "boundsOrigin") {
70                 private Rect mBounds = new Rect();
71 
72                 @Override
73                 public void set(Drawable object, PointF value) {
74                     object.copyBounds(mBounds);
75                     mBounds.offsetTo(Math.round(value.x), Math.round(value.y));
76                     object.setBounds(mBounds);
77                 }
78 
79                 @Override
80                 public PointF get(Drawable object) {
81                     object.copyBounds(mBounds);
82                     return new PointF(mBounds.left, mBounds.top);
83                 }
84     };
85 
86     private static final Property<ViewBounds, PointF> TOP_LEFT_PROPERTY =
87             new Property<ViewBounds, PointF>(PointF.class, "topLeft") {
88                 @Override
89                 public void set(ViewBounds viewBounds, PointF topLeft) {
90                     viewBounds.setTopLeft(topLeft);
91                 }
92 
93                 @Override
94                 public PointF get(ViewBounds viewBounds) {
95                     return null;
96                 }
97             };
98 
99     private static final Property<ViewBounds, PointF> BOTTOM_RIGHT_PROPERTY =
100             new Property<ViewBounds, PointF>(PointF.class, "bottomRight") {
101                 @Override
102                 public void set(ViewBounds viewBounds, PointF bottomRight) {
103                     viewBounds.setBottomRight(bottomRight);
104                 }
105 
106                 @Override
107                 public PointF get(ViewBounds viewBounds) {
108                     return null;
109                 }
110             };
111 
112     private static final Property<View, PointF> BOTTOM_RIGHT_ONLY_PROPERTY =
113             new Property<View, PointF>(PointF.class, "bottomRight") {
114                 @Override
115                 public void set(View view, PointF bottomRight) {
116                     int left = view.getLeft();
117                     int top = view.getTop();
118                     int right = Math.round(bottomRight.x);
119                     int bottom = Math.round(bottomRight.y);
120                     view.setLeftTopRightBottom(left, top, right, bottom);
121                 }
122 
123                 @Override
124                 public PointF get(View view) {
125                     return null;
126                 }
127             };
128 
129     private static final Property<View, PointF> TOP_LEFT_ONLY_PROPERTY =
130             new Property<View, PointF>(PointF.class, "topLeft") {
131                 @Override
132                 public void set(View view, PointF topLeft) {
133                     int left = Math.round(topLeft.x);
134                     int top = Math.round(topLeft.y);
135                     int right = view.getRight();
136                     int bottom = view.getBottom();
137                     view.setLeftTopRightBottom(left, top, right, bottom);
138                 }
139 
140                 @Override
141                 public PointF get(View view) {
142                     return null;
143                 }
144             };
145 
146     private static final Property<View, PointF> POSITION_PROPERTY =
147             new Property<View, PointF>(PointF.class, "position") {
148                 @Override
149                 public void set(View view, PointF topLeft) {
150                     int left = Math.round(topLeft.x);
151                     int top = Math.round(topLeft.y);
152                     int right = left + view.getWidth();
153                     int bottom = top + view.getHeight();
154                     view.setLeftTopRightBottom(left, top, right, bottom);
155                 }
156 
157                 @Override
158                 public PointF get(View view) {
159                     return null;
160                 }
161             };
162 
163     int[] tempLocation = new int[2];
164     boolean mResizeClip = false;
165     boolean mReparent = false;
166     private static final String LOG_TAG = "ChangeBounds";
167 
168     private static RectEvaluator sRectEvaluator = new RectEvaluator();
169 
ChangeBounds()170     public ChangeBounds() {}
171 
ChangeBounds(Context context, AttributeSet attrs)172     public ChangeBounds(Context context, AttributeSet attrs) {
173         super(context, attrs);
174 
175         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ChangeBounds);
176         boolean resizeClip = a.getBoolean(R.styleable.ChangeBounds_resizeClip, false);
177         a.recycle();
178         setResizeClip(resizeClip);
179     }
180 
181     @Override
getTransitionProperties()182     public String[] getTransitionProperties() {
183         return sTransitionProperties;
184     }
185 
186     /**
187      * When <code>resizeClip</code> is true, ChangeBounds resizes the view using the clipBounds
188      * instead of changing the dimensions of the view during the animation. When
189      * <code>resizeClip</code> is false, ChangeBounds resizes the View by changing its dimensions.
190      *
191      * <p>When resizeClip is set to true, the clip bounds is modified by ChangeBounds. Therefore,
192      * {@link android.transition.ChangeClipBounds} is not compatible with ChangeBounds
193      * in this mode.</p>
194      *
195      * @param resizeClip Used to indicate whether the view bounds should be modified or the
196      *                   clip bounds should be modified by ChangeBounds.
197      * @see android.view.View#setClipBounds(android.graphics.Rect)
198      * @attr ref android.R.styleable#ChangeBounds_resizeClip
199      */
setResizeClip(boolean resizeClip)200     public void setResizeClip(boolean resizeClip) {
201         mResizeClip = resizeClip;
202     }
203 
204     /**
205      * Returns true when the ChangeBounds will resize by changing the clip bounds during the
206      * view animation or false when bounds are changed. The default value is false.
207      *
208      * @return 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      * @attr ref android.R.styleable#ChangeBounds_resizeClip
211      */
getResizeClip()212     public boolean getResizeClip() {
213         return mResizeClip;
214     }
215 
216     /**
217      * Setting this flag tells ChangeBounds to track the before/after parent
218      * of every view using this transition. The flag is not enabled by
219      * default because it requires the parent instances to be the same
220      * in the two scenes or else all parents must use ids to allow
221      * the transition to determine which parents are the same.
222      *
223      * @param reparent true if the transition should track the parent
224      * container of target views and animate parent changes.
225      * @deprecated Use {@link android.transition.ChangeTransform} to handle
226      * transitions between different parents.
227      */
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                         }
419 
420                         @Override
421                         public void onTransitionPause(Transition transition) {
422                             parent.suppressLayout(false);
423                         }
424 
425                         @Override
426                         public void onTransitionResume(Transition transition) {
427                             parent.suppressLayout(true);
428                         }
429                     };
430                     addListener(transitionListener);
431                 }
432                 return anim;
433             }
434         } else {
435             sceneRoot.getLocationInWindow(tempLocation);
436             int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X) - tempLocation[0];
437             int startY = (Integer) startValues.values.get(PROPNAME_WINDOW_Y) - tempLocation[1];
438             int endX = (Integer) endValues.values.get(PROPNAME_WINDOW_X) - tempLocation[0];
439             int endY = (Integer) endValues.values.get(PROPNAME_WINDOW_Y) - tempLocation[1];
440             // TODO: also handle size changes: check bounds and animate size changes
441             if (startX != endX || startY != endY) {
442                 final int width = view.getWidth();
443                 final int height = view.getHeight();
444                 Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
445                 Canvas canvas = new Canvas(bitmap);
446                 view.draw(canvas);
447                 final BitmapDrawable drawable = new BitmapDrawable(bitmap);
448                 drawable.setBounds(startX, startY, startX + width, startY + height);
449                 final float transitionAlpha = view.getTransitionAlpha();
450                 view.setTransitionAlpha(0);
451                 sceneRoot.getOverlay().add(drawable);
452                 Path topLeftPath = getPathMotion().getPath(startX, startY, endX, endY);
453                 PropertyValuesHolder origin = PropertyValuesHolder.ofObject(
454                         DRAWABLE_ORIGIN_PROPERTY, null, topLeftPath);
455                 ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, origin);
456                 anim.addListener(new AnimatorListenerAdapter() {
457                     @Override
458                     public void onAnimationEnd(Animator animation) {
459                         sceneRoot.getOverlay().remove(drawable);
460                         view.setTransitionAlpha(transitionAlpha);
461                     }
462                 });
463                 return anim;
464             }
465         }
466         return null;
467     }
468 
469     private static class ViewBounds {
470         private int mLeft;
471         private int mTop;
472         private int mRight;
473         private int mBottom;
474         private boolean mIsTopLeftSet;
475         private boolean mIsBottomRightSet;
476         private View mView;
477 
ViewBounds(View view)478         public ViewBounds(View view) {
479             mView = view;
480         }
481 
setTopLeft(PointF topLeft)482         public void setTopLeft(PointF topLeft) {
483             mLeft = Math.round(topLeft.x);
484             mTop = Math.round(topLeft.y);
485             mIsTopLeftSet = true;
486             if (mIsBottomRightSet) {
487                 setLeftTopRightBottom();
488             }
489         }
490 
setBottomRight(PointF bottomRight)491         public void setBottomRight(PointF bottomRight) {
492             mRight = Math.round(bottomRight.x);
493             mBottom = Math.round(bottomRight.y);
494             mIsBottomRightSet = true;
495             if (mIsTopLeftSet) {
496                 setLeftTopRightBottom();
497             }
498         }
499 
setLeftTopRightBottom()500         private void setLeftTopRightBottom() {
501             mView.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
502             mIsTopLeftSet = false;
503             mIsBottomRightSet = false;
504         }
505     }
506 }
507