Home | History | Annotate | Download | only in transition
      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 
    169     public ChangeBounds() {
    170     }
    171 
    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
    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      */
    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      */
    212     public boolean getResizeClip() {
    213         return mResizeClip;
    214     }
    215 
    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
    235     public void captureStartValues(@NonNull TransitionValues transitionValues) {
    236         captureValues(transitionValues);
    237     }
    238 
    239     @Override
    240     public void captureEndValues(@NonNull TransitionValues transitionValues) {
    241         captureValues(transitionValues);
    242     }
    243 
    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
    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 
    468         ViewBounds(View view) {
    469             mView = view;
    470         }
    471 
    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 
    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 
    490         private void setLeftTopRightBottom() {
    491             ViewUtils.setLeftTopRightBottom(mView, mLeft, mTop, mRight, mBottom);
    492             mTopLeftCalls = 0;
    493             mBottomRightCalls = 0;
    494         }
    495 
    496     }
    497 
    498 }
    499