• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 androidx.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.content.Context;
25 import android.content.res.TypedArray;
26 import android.content.res.XmlResourceParser;
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 androidx.annotation.NonNull;
40 import androidx.annotation.Nullable;
41 import androidx.core.content.res.TypedArrayUtils;
42 import androidx.core.view.ViewCompat;
43 
44 import java.util.Map;
45 
46 /**
47  * This transition captures the layout bounds of target views before and after
48  * the scene change and animates those changes during the transition.
49  *
50  * <p>A ChangeBounds transition can be described in a resource file by using the
51  * tag <code>changeBounds</code>, along with the other standard attributes of 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                     ViewUtils.setLeftTopRightBottom(view, 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                     ViewUtils.setLeftTopRightBottom(view, 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                     ViewUtils.setLeftTopRightBottom(view, left, top, right, bottom);
155                 }
156 
157                 @Override
158                 public PointF get(View view) {
159                     return null;
160                 }
161             };
162 
163     private int[] mTempLocation = new int[2];
164     private boolean mResizeClip = false;
165     private boolean mReparent = false;
166 
167     private static RectEvaluator sRectEvaluator = new RectEvaluator();
168 
ChangeBounds()169     public ChangeBounds() {
170     }
171 
ChangeBounds(Context context, AttributeSet attrs)172     public ChangeBounds(Context context, AttributeSet attrs) {
173         super(context, attrs);
174 
175         TypedArray a = context.obtainStyledAttributes(attrs, Styleable.CHANGE_BOUNDS);
176         boolean resizeClip = TypedArrayUtils.getNamedBoolean(a, (XmlResourceParser) attrs,
177                 "resizeClip", Styleable.ChangeBounds.RESIZE_CLIP, false);
178         a.recycle();
179         setResizeClip(resizeClip);
180     }
181 
182     @Nullable
183     @Override
getTransitionProperties()184     public String[] getTransitionProperties() {
185         return sTransitionProperties;
186     }
187 
188     /**
189      * When <code>resizeClip</code> is true, ChangeBounds resizes the view using the clipBounds
190      * instead of changing the dimensions of the view during the animation. When
191      * <code>resizeClip</code> is false, ChangeBounds resizes the View by changing its dimensions.
192      *
193      * <p>When resizeClip is set to true, the clip bounds is modified by ChangeBounds. Therefore,
194      * {@link android.transition.ChangeClipBounds} is not compatible with ChangeBounds
195      * in this mode.</p>
196      *
197      * @param resizeClip Used to indicate whether the view bounds should be modified or the
198      *                   clip bounds should be modified by ChangeBounds.
199      * @see android.view.View#setClipBounds(android.graphics.Rect)
200      */
setResizeClip(boolean resizeClip)201     public void setResizeClip(boolean resizeClip) {
202         mResizeClip = resizeClip;
203     }
204 
205     /**
206      * Returns true when the ChangeBounds will resize by changing the clip bounds during the
207      * view animation or false when bounds are changed. The default value is false.
208      *
209      * @return true when the ChangeBounds will resize by changing the clip bounds during the
210      * view animation or false when bounds are changed. The default value is false.
211      */
getResizeClip()212     public boolean getResizeClip() {
213         return mResizeClip;
214     }
215 
captureValues(TransitionValues values)216     private void captureValues(TransitionValues values) {
217         View view = values.view;
218 
219         if (ViewCompat.isLaidOut(view) || view.getWidth() != 0 || view.getHeight() != 0) {
220             values.values.put(PROPNAME_BOUNDS, new Rect(view.getLeft(), view.getTop(),
221                     view.getRight(), view.getBottom()));
222             values.values.put(PROPNAME_PARENT, values.view.getParent());
223             if (mReparent) {
224                 values.view.getLocationInWindow(mTempLocation);
225                 values.values.put(PROPNAME_WINDOW_X, mTempLocation[0]);
226                 values.values.put(PROPNAME_WINDOW_Y, mTempLocation[1]);
227             }
228             if (mResizeClip) {
229                 values.values.put(PROPNAME_CLIP, ViewCompat.getClipBounds(view));
230             }
231         }
232     }
233 
234     @Override
captureStartValues(@onNull TransitionValues transitionValues)235     public void captureStartValues(@NonNull TransitionValues transitionValues) {
236         captureValues(transitionValues);
237     }
238 
239     @Override
captureEndValues(@onNull TransitionValues transitionValues)240     public void captureEndValues(@NonNull TransitionValues transitionValues) {
241         captureValues(transitionValues);
242     }
243 
parentMatches(View startParent, View endParent)244     private boolean parentMatches(View startParent, View endParent) {
245         boolean parentMatches = true;
246         if (mReparent) {
247             TransitionValues endValues = getMatchedTransitionValues(startParent, true);
248             if (endValues == null) {
249                 parentMatches = startParent == endParent;
250             } else {
251                 parentMatches = endParent == endValues.view;
252             }
253         }
254         return parentMatches;
255     }
256 
257     @Override
258     @Nullable
createAnimator(@onNull final ViewGroup sceneRoot, @Nullable TransitionValues startValues, @Nullable TransitionValues endValues)259     public Animator createAnimator(@NonNull final ViewGroup sceneRoot,
260             @Nullable TransitionValues startValues, @Nullable TransitionValues endValues) {
261         if (startValues == null || endValues == null) {
262             return null;
263         }
264         Map<String, Object> startParentVals = startValues.values;
265         Map<String, Object> endParentVals = endValues.values;
266         ViewGroup startParent = (ViewGroup) startParentVals.get(PROPNAME_PARENT);
267         ViewGroup endParent = (ViewGroup) endParentVals.get(PROPNAME_PARENT);
268         if (startParent == null || endParent == null) {
269             return null;
270         }
271         final View view = endValues.view;
272         if (parentMatches(startParent, endParent)) {
273             Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS);
274             Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS);
275             final int startLeft = startBounds.left;
276             final int endLeft = endBounds.left;
277             final int startTop = startBounds.top;
278             final int endTop = endBounds.top;
279             final int startRight = startBounds.right;
280             final int endRight = endBounds.right;
281             final int startBottom = startBounds.bottom;
282             final int endBottom = endBounds.bottom;
283             final int startWidth = startRight - startLeft;
284             final int startHeight = startBottom - startTop;
285             final int endWidth = endRight - endLeft;
286             final int endHeight = endBottom - endTop;
287             Rect startClip = (Rect) startValues.values.get(PROPNAME_CLIP);
288             Rect endClip = (Rect) endValues.values.get(PROPNAME_CLIP);
289             int numChanges = 0;
290             if ((startWidth != 0 && startHeight != 0) || (endWidth != 0 && endHeight != 0)) {
291                 if (startLeft != endLeft || startTop != endTop) ++numChanges;
292                 if (startRight != endRight || startBottom != endBottom) ++numChanges;
293             }
294             if ((startClip != null && !startClip.equals(endClip))
295                     || (startClip == null && endClip != null)) {
296                 ++numChanges;
297             }
298             if (numChanges > 0) {
299                 Animator anim;
300                 if (!mResizeClip) {
301                     ViewUtils.setLeftTopRightBottom(view, startLeft, startTop, startRight,
302                             startBottom);
303                     if (numChanges == 2) {
304                         if (startWidth == endWidth && startHeight == endHeight) {
305                             Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft,
306                                     endTop);
307                             anim = ObjectAnimatorUtils.ofPointF(view, POSITION_PROPERTY,
308                                     topLeftPath);
309                         } else {
310                             final ViewBounds viewBounds = new ViewBounds(view);
311                             Path topLeftPath = getPathMotion().getPath(startLeft, startTop,
312                                     endLeft, endTop);
313                             ObjectAnimator topLeftAnimator = ObjectAnimatorUtils
314                                     .ofPointF(viewBounds, TOP_LEFT_PROPERTY, topLeftPath);
315 
316                             Path bottomRightPath = getPathMotion().getPath(startRight, startBottom,
317                                     endRight, endBottom);
318                             ObjectAnimator bottomRightAnimator = ObjectAnimatorUtils.ofPointF(
319                                     viewBounds, BOTTOM_RIGHT_PROPERTY, bottomRightPath);
320                             AnimatorSet set = new AnimatorSet();
321                             set.playTogether(topLeftAnimator, bottomRightAnimator);
322                             anim = set;
323                             set.addListener(new AnimatorListenerAdapter() {
324                                 // We need a strong reference to viewBounds until the
325                                 // animator ends (The ObjectAnimator holds only a weak reference).
326                                 @SuppressWarnings("unused")
327                                 private ViewBounds mViewBounds = viewBounds;
328                             });
329                         }
330                     } else if (startLeft != endLeft || startTop != endTop) {
331                         Path topLeftPath = getPathMotion().getPath(startLeft, startTop,
332                                 endLeft, endTop);
333                         anim = ObjectAnimatorUtils.ofPointF(view, TOP_LEFT_ONLY_PROPERTY,
334                                 topLeftPath);
335                     } else {
336                         Path bottomRight = getPathMotion().getPath(startRight, startBottom,
337                                 endRight, endBottom);
338                         anim = ObjectAnimatorUtils.ofPointF(view, BOTTOM_RIGHT_ONLY_PROPERTY,
339                                 bottomRight);
340                     }
341                 } else {
342                     int maxWidth = Math.max(startWidth, endWidth);
343                     int maxHeight = Math.max(startHeight, endHeight);
344 
345                     ViewUtils.setLeftTopRightBottom(view, startLeft, startTop, startLeft + maxWidth,
346                             startTop + maxHeight);
347 
348                     ObjectAnimator positionAnimator = null;
349                     if (startLeft != endLeft || startTop != endTop) {
350                         Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft,
351                                 endTop);
352                         positionAnimator = ObjectAnimatorUtils.ofPointF(view, POSITION_PROPERTY,
353                                 topLeftPath);
354                     }
355                     final Rect finalClip = endClip;
356                     if (startClip == null) {
357                         startClip = new Rect(0, 0, startWidth, startHeight);
358                     }
359                     if (endClip == null) {
360                         endClip = new Rect(0, 0, endWidth, endHeight);
361                     }
362                     ObjectAnimator clipAnimator = null;
363                     if (!startClip.equals(endClip)) {
364                         ViewCompat.setClipBounds(view, startClip);
365                         clipAnimator = ObjectAnimator.ofObject(view, "clipBounds", sRectEvaluator,
366                                 startClip, endClip);
367                         clipAnimator.addListener(new AnimatorListenerAdapter() {
368                             private boolean mIsCanceled;
369 
370                             @Override
371                             public void onAnimationCancel(Animator animation) {
372                                 mIsCanceled = true;
373                             }
374 
375                             @Override
376                             public void onAnimationEnd(Animator animation) {
377                                 if (!mIsCanceled) {
378                                     ViewCompat.setClipBounds(view, finalClip);
379                                     ViewUtils.setLeftTopRightBottom(view, endLeft, endTop, endRight,
380                                             endBottom);
381                                 }
382                             }
383                         });
384                     }
385                     anim = TransitionUtils.mergeAnimators(positionAnimator,
386                             clipAnimator);
387                 }
388                 if (view.getParent() instanceof ViewGroup) {
389                     final ViewGroup parent = (ViewGroup) view.getParent();
390                     ViewGroupUtils.suppressLayout(parent, true);
391                     TransitionListener transitionListener = new TransitionListenerAdapter() {
392                         boolean mCanceled = false;
393 
394                         @Override
395                         public void onTransitionCancel(@NonNull Transition transition) {
396                             ViewGroupUtils.suppressLayout(parent, false);
397                             mCanceled = true;
398                         }
399 
400                         @Override
401                         public void onTransitionEnd(@NonNull Transition transition) {
402                             if (!mCanceled) {
403                                 ViewGroupUtils.suppressLayout(parent, false);
404                             }
405                             transition.removeListener(this);
406                         }
407 
408                         @Override
409                         public void onTransitionPause(@NonNull Transition transition) {
410                             ViewGroupUtils.suppressLayout(parent, false);
411                         }
412 
413                         @Override
414                         public void onTransitionResume(@NonNull Transition transition) {
415                             ViewGroupUtils.suppressLayout(parent, true);
416                         }
417                     };
418                     addListener(transitionListener);
419                 }
420                 return anim;
421             }
422         } else {
423             int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X);
424             int startY = (Integer) startValues.values.get(PROPNAME_WINDOW_Y);
425             int endX = (Integer) endValues.values.get(PROPNAME_WINDOW_X);
426             int endY = (Integer) endValues.values.get(PROPNAME_WINDOW_Y);
427             // TODO: also handle size changes: check bounds and animate size changes
428             if (startX != endX || startY != endY) {
429                 sceneRoot.getLocationInWindow(mTempLocation);
430                 Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(),
431                         Bitmap.Config.ARGB_8888);
432                 Canvas canvas = new Canvas(bitmap);
433                 view.draw(canvas);
434                 @SuppressWarnings("deprecation") final BitmapDrawable drawable = new BitmapDrawable(
435                         bitmap);
436                 final float transitionAlpha = ViewUtils.getTransitionAlpha(view);
437                 ViewUtils.setTransitionAlpha(view, 0);
438                 ViewUtils.getOverlay(sceneRoot).add(drawable);
439                 Path topLeftPath = getPathMotion().getPath(startX - mTempLocation[0],
440                         startY - mTempLocation[1], endX - mTempLocation[0],
441                         endY - mTempLocation[1]);
442                 PropertyValuesHolder origin = PropertyValuesHolderUtils.ofPointF(
443                         DRAWABLE_ORIGIN_PROPERTY, topLeftPath);
444                 ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, origin);
445                 anim.addListener(new AnimatorListenerAdapter() {
446                     @Override
447                     public void onAnimationEnd(Animator animation) {
448                         ViewUtils.getOverlay(sceneRoot).remove(drawable);
449                         ViewUtils.setTransitionAlpha(view, transitionAlpha);
450                     }
451                 });
452                 return anim;
453             }
454         }
455         return null;
456     }
457 
458     private static class ViewBounds {
459 
460         private int mLeft;
461         private int mTop;
462         private int mRight;
463         private int mBottom;
464         private View mView;
465         private int mTopLeftCalls;
466         private int mBottomRightCalls;
467 
ViewBounds(View view)468         ViewBounds(View view) {
469             mView = view;
470         }
471 
setTopLeft(PointF topLeft)472         void setTopLeft(PointF topLeft) {
473             mLeft = Math.round(topLeft.x);
474             mTop = Math.round(topLeft.y);
475             mTopLeftCalls++;
476             if (mTopLeftCalls == mBottomRightCalls) {
477                 setLeftTopRightBottom();
478             }
479         }
480 
setBottomRight(PointF bottomRight)481         void setBottomRight(PointF bottomRight) {
482             mRight = Math.round(bottomRight.x);
483             mBottom = Math.round(bottomRight.y);
484             mBottomRightCalls++;
485             if (mTopLeftCalls == mBottomRightCalls) {
486                 setLeftTopRightBottom();
487             }
488         }
489 
setLeftTopRightBottom()490         private void setLeftTopRightBottom() {
491             ViewUtils.setLeftTopRightBottom(mView, mLeft, mTop, mRight, mBottom);
492             mTopLeftCalls = 0;
493             mBottomRightCalls = 0;
494         }
495 
496     }
497 
498 }
499