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