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