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