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.content.Context;
     20 import android.graphics.PointF;
     21 
     22 import android.animation.Animator;
     23 import android.animation.AnimatorListenerAdapter;
     24 import android.animation.ObjectAnimator;
     25 import android.animation.PropertyValuesHolder;
     26 import android.animation.RectEvaluator;
     27 import android.graphics.Bitmap;
     28 import android.graphics.Canvas;
     29 import android.graphics.Path;
     30 import android.graphics.Rect;
     31 import android.graphics.drawable.BitmapDrawable;
     32 import android.graphics.drawable.Drawable;
     33 import android.util.AttributeSet;
     34 import android.util.IntProperty;
     35 import android.util.Property;
     36 import android.view.View;
     37 import android.view.ViewGroup;
     38 
     39 import java.util.Map;
     40 
     41 /**
     42  * This transition captures the layout bounds of target views before and after
     43  * the scene change and animates those changes during the transition.
     44  *
     45  * <p>A ChangeBounds transition can be described in a resource file by using the
     46  * tag <code>changeBounds</code>, along with the other standard
     47  * attributes of {@link android.R.styleable#Transition}.</p>
     48  */
     49 public class ChangeBounds extends Transition {
     50 
     51     private static final String PROPNAME_BOUNDS = "android:changeBounds:bounds";
     52     private static final String PROPNAME_PARENT = "android:changeBounds:parent";
     53     private static final String PROPNAME_WINDOW_X = "android:changeBounds:windowX";
     54     private static final String PROPNAME_WINDOW_Y = "android:changeBounds:windowY";
     55     private static final String[] sTransitionProperties = {
     56             PROPNAME_BOUNDS,
     57             PROPNAME_PARENT,
     58             PROPNAME_WINDOW_X,
     59             PROPNAME_WINDOW_Y
     60     };
     61 
     62     private static final Property<Drawable, PointF> DRAWABLE_ORIGIN_PROPERTY =
     63             new Property<Drawable, PointF>(PointF.class, "boundsOrigin") {
     64                 private Rect mBounds = new Rect();
     65 
     66                 @Override
     67                 public void set(Drawable object, PointF value) {
     68                     object.copyBounds(mBounds);
     69                     mBounds.offsetTo(Math.round(value.x), Math.round(value.y));
     70                     object.setBounds(mBounds);
     71                 }
     72 
     73                 @Override
     74                 public PointF get(Drawable object) {
     75                     object.copyBounds(mBounds);
     76                     return new PointF(mBounds.left, mBounds.top);
     77                 }
     78     };
     79 
     80     int[] tempLocation = new int[2];
     81     boolean mResizeClip = false;
     82     boolean mReparent = false;
     83     private static final String LOG_TAG = "ChangeBounds";
     84 
     85     private static RectEvaluator sRectEvaluator = new RectEvaluator();
     86 
     87     public ChangeBounds() {}
     88 
     89     public ChangeBounds(Context context, AttributeSet attrs) {
     90         super(context, attrs);
     91     }
     92 
     93     @Override
     94     public String[] getTransitionProperties() {
     95         return sTransitionProperties;
     96     }
     97 
     98     public void setResizeClip(boolean resizeClip) {
     99         mResizeClip = resizeClip;
    100     }
    101 
    102     /**
    103      * Setting this flag tells ChangeBounds to track the before/after parent
    104      * of every view using this transition. The flag is not enabled by
    105      * default because it requires the parent instances to be the same
    106      * in the two scenes or else all parents must use ids to allow
    107      * the transition to determine which parents are the same.
    108      *
    109      * @param reparent true if the transition should track the parent
    110      * container of target views and animate parent changes.
    111      * @deprecated Use {@link android.transition.ChangeTransform} to handle
    112      * transitions between different parents.
    113      */
    114     public void setReparent(boolean reparent) {
    115         mReparent = reparent;
    116     }
    117 
    118     private void captureValues(TransitionValues values) {
    119         View view = values.view;
    120 
    121         if (view.isLaidOut() || view.getWidth() != 0 || view.getHeight() != 0) {
    122             values.values.put(PROPNAME_BOUNDS, new Rect(view.getLeft(), view.getTop(),
    123                     view.getRight(), view.getBottom()));
    124             values.values.put(PROPNAME_PARENT, values.view.getParent());
    125             if (mReparent) {
    126                 values.view.getLocationInWindow(tempLocation);
    127                 values.values.put(PROPNAME_WINDOW_X, tempLocation[0]);
    128                 values.values.put(PROPNAME_WINDOW_Y, tempLocation[1]);
    129             }
    130         }
    131     }
    132 
    133     @Override
    134     public void captureStartValues(TransitionValues transitionValues) {
    135         captureValues(transitionValues);
    136     }
    137 
    138     @Override
    139     public void captureEndValues(TransitionValues transitionValues) {
    140         captureValues(transitionValues);
    141     }
    142 
    143     private boolean parentMatches(View startParent, View endParent) {
    144         boolean parentMatches = true;
    145         if (mReparent) {
    146             TransitionValues endValues = getMatchedTransitionValues(startParent, true);
    147             if (endValues == null) {
    148                 parentMatches = startParent == endParent;
    149             } else {
    150                 parentMatches = endParent == endValues.view;
    151             }
    152         }
    153         return parentMatches;
    154     }
    155 
    156     @Override
    157     public Animator createAnimator(final ViewGroup sceneRoot, TransitionValues startValues,
    158             TransitionValues endValues) {
    159         if (startValues == null || endValues == null) {
    160             return null;
    161         }
    162         Map<String, Object> startParentVals = startValues.values;
    163         Map<String, Object> endParentVals = endValues.values;
    164         ViewGroup startParent = (ViewGroup) startParentVals.get(PROPNAME_PARENT);
    165         ViewGroup endParent = (ViewGroup) endParentVals.get(PROPNAME_PARENT);
    166         if (startParent == null || endParent == null) {
    167             return null;
    168         }
    169         final View view = endValues.view;
    170         if (parentMatches(startParent, endParent)) {
    171             Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS);
    172             Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS);
    173             int startLeft = startBounds.left;
    174             int endLeft = endBounds.left;
    175             int startTop = startBounds.top;
    176             int endTop = endBounds.top;
    177             int startRight = startBounds.right;
    178             int endRight = endBounds.right;
    179             int startBottom = startBounds.bottom;
    180             int endBottom = endBounds.bottom;
    181             int startWidth = startRight - startLeft;
    182             int startHeight = startBottom - startTop;
    183             int endWidth = endRight - endLeft;
    184             int endHeight = endBottom - endTop;
    185             int numChanges = 0;
    186             if ((startWidth != 0 && startHeight != 0) || (endWidth != 0 && endHeight != 0)) {
    187                 if (startLeft != endLeft || startTop != endTop) ++numChanges;
    188                 if (startRight != endRight || startBottom != endBottom) ++numChanges;
    189             }
    190             if (numChanges > 0) {
    191                 if (!mResizeClip) {
    192                     Animator anim;
    193                     if (startWidth == endWidth && startHeight == endHeight) {
    194                         view.offsetLeftAndRight(startLeft - view.getLeft());
    195                         view.offsetTopAndBottom(startTop - view.getTop());
    196                         Path positionPath = getPathMotion().getPath(0, 0, endLeft - startLeft,
    197                                 endTop - startTop);
    198                         anim = ObjectAnimator.ofInt(view, new HorizontalOffsetProperty(),
    199                                 new VerticalOffsetProperty(), positionPath);
    200                     } else {
    201                         if (startLeft != endLeft) view.setLeft(startLeft);
    202                         if (startTop != endTop) view.setTop(startTop);
    203                         if (startRight != endRight) view.setRight(startRight);
    204                         if (startBottom != endBottom) view.setBottom(startBottom);
    205                         ObjectAnimator topLeftAnimator = null;
    206                         if (startLeft != endLeft || startTop != endTop) {
    207                             Path topLeftPath = getPathMotion().getPath(startLeft, startTop,
    208                                     endLeft, endTop);
    209                             topLeftAnimator = ObjectAnimator
    210                                     .ofInt(view, "left", "top", topLeftPath);
    211                         }
    212                         ObjectAnimator bottomRightAnimator = null;
    213                         if (startRight != endRight || startBottom != endBottom) {
    214                             Path bottomRightPath = getPathMotion().getPath(startRight, startBottom,
    215                                     endRight, endBottom);
    216                             bottomRightAnimator = ObjectAnimator.ofInt(view, "right", "bottom",
    217                                     bottomRightPath);
    218                         }
    219                         anim = TransitionUtils.mergeAnimators(topLeftAnimator,
    220                                 bottomRightAnimator);
    221                     }
    222                     if (view.getParent() instanceof ViewGroup) {
    223                         final ViewGroup parent = (ViewGroup) view.getParent();
    224                         parent.suppressLayout(true);
    225                         TransitionListener transitionListener = new TransitionListenerAdapter() {
    226                             boolean mCanceled = false;
    227 
    228                             @Override
    229                             public void onTransitionCancel(Transition transition) {
    230                                 parent.suppressLayout(false);
    231                                 mCanceled = true;
    232                             }
    233 
    234                             @Override
    235                             public void onTransitionEnd(Transition transition) {
    236                                 if (!mCanceled) {
    237                                     parent.suppressLayout(false);
    238                                 }
    239                             }
    240 
    241                             @Override
    242                             public void onTransitionPause(Transition transition) {
    243                                 parent.suppressLayout(false);
    244                             }
    245 
    246                             @Override
    247                             public void onTransitionResume(Transition transition) {
    248                                 parent.suppressLayout(true);
    249                             }
    250                         };
    251                         addListener(transitionListener);
    252                     }
    253                     return anim;
    254                 } else {
    255                     if (startWidth != endWidth) view.setRight(endLeft +
    256                             Math.max(startWidth, endWidth));
    257                     if (startHeight != endHeight) view.setBottom(endTop +
    258                             Math.max(startHeight, endHeight));
    259                     // TODO: don't clobber TX/TY
    260                     if (startLeft != endLeft) view.setTranslationX(startLeft - endLeft);
    261                     if (startTop != endTop) view.setTranslationY(startTop - endTop);
    262                     // Animate location with translationX/Y and size with clip bounds
    263                     float transXDelta = endLeft - startLeft;
    264                     float transYDelta = endTop - startTop;
    265                     int widthDelta = endWidth - startWidth;
    266                     int heightDelta = endHeight - startHeight;
    267                     numChanges = 0;
    268                     if (transXDelta != 0) numChanges++;
    269                     if (transYDelta != 0) numChanges++;
    270                     if (widthDelta != 0 || heightDelta != 0) numChanges++;
    271                     ObjectAnimator translationAnimator = null;
    272                     if (transXDelta != 0 || transYDelta != 0) {
    273                         Path topLeftPath = getPathMotion().getPath(0, 0, transXDelta, transYDelta);
    274                         translationAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X,
    275                                 View.TRANSLATION_Y, topLeftPath);
    276                     }
    277                     ObjectAnimator clipAnimator = null;
    278                     if (widthDelta != 0 || heightDelta != 0) {
    279                         Rect tempStartBounds = new Rect(0, 0, startWidth, startHeight);
    280                         Rect tempEndBounds = new Rect(0, 0, endWidth, endHeight);
    281                         clipAnimator = ObjectAnimator.ofObject(view, "clipBounds", sRectEvaluator,
    282                                 tempStartBounds, tempEndBounds);
    283                     }
    284                     Animator anim = TransitionUtils.mergeAnimators(translationAnimator,
    285                             clipAnimator);
    286                     if (view.getParent() instanceof ViewGroup) {
    287                         final ViewGroup parent = (ViewGroup) view.getParent();
    288                         parent.suppressLayout(true);
    289                         TransitionListener transitionListener = new TransitionListenerAdapter() {
    290                             boolean mCanceled = false;
    291 
    292                             @Override
    293                             public void onTransitionCancel(Transition transition) {
    294                                 parent.suppressLayout(false);
    295                                 mCanceled = true;
    296                             }
    297 
    298                             @Override
    299                             public void onTransitionEnd(Transition transition) {
    300                                 if (!mCanceled) {
    301                                     parent.suppressLayout(false);
    302                                 }
    303                             }
    304 
    305                             @Override
    306                             public void onTransitionPause(Transition transition) {
    307                                 parent.suppressLayout(false);
    308                             }
    309 
    310                             @Override
    311                             public void onTransitionResume(Transition transition) {
    312                                 parent.suppressLayout(true);
    313                             }
    314                         };
    315                         addListener(transitionListener);
    316                     }
    317                     anim.addListener(new AnimatorListenerAdapter() {
    318                         @Override
    319                         public void onAnimationEnd(Animator animation) {
    320                             view.setClipBounds(null);
    321                         }
    322                     });
    323                     return anim;
    324                 }
    325             }
    326         } else {
    327             int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X);
    328             int startY = (Integer) startValues.values.get(PROPNAME_WINDOW_Y);
    329             int endX = (Integer) endValues.values.get(PROPNAME_WINDOW_X);
    330             int endY = (Integer) endValues.values.get(PROPNAME_WINDOW_Y);
    331             // TODO: also handle size changes: check bounds and animate size changes
    332             if (startX != endX || startY != endY) {
    333                 sceneRoot.getLocationInWindow(tempLocation);
    334                 Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(),
    335                         Bitmap.Config.ARGB_8888);
    336                 Canvas canvas = new Canvas(bitmap);
    337                 view.draw(canvas);
    338                 final BitmapDrawable drawable = new BitmapDrawable(bitmap);
    339                 final float transitionAlpha = view.getTransitionAlpha();
    340                 view.setTransitionAlpha(0);
    341                 sceneRoot.getOverlay().add(drawable);
    342                 Path topLeftPath = getPathMotion().getPath(startX - tempLocation[0],
    343                         startY - tempLocation[1], endX - tempLocation[0], endY - tempLocation[1]);
    344                 PropertyValuesHolder origin = PropertyValuesHolder.ofObject(
    345                         DRAWABLE_ORIGIN_PROPERTY, null, topLeftPath);
    346                 ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, origin);
    347                 anim.addListener(new AnimatorListenerAdapter() {
    348                     @Override
    349                     public void onAnimationEnd(Animator animation) {
    350                         sceneRoot.getOverlay().remove(drawable);
    351                         view.setTransitionAlpha(transitionAlpha);
    352                     }
    353                 });
    354                 return anim;
    355             }
    356         }
    357         return null;
    358     }
    359 
    360     private abstract static class OffsetProperty extends IntProperty<View> {
    361         int mPreviousValue;
    362 
    363         public OffsetProperty(String name) {
    364             super(name);
    365         }
    366 
    367         @Override
    368         public void setValue(View view, int value) {
    369             int offset = value - mPreviousValue;
    370             offsetBy(view, offset);
    371             mPreviousValue = value;
    372         }
    373 
    374         @Override
    375         public Integer get(View object) {
    376             return null;
    377         }
    378 
    379         protected abstract void offsetBy(View view, int by);
    380     }
    381 
    382     private static class HorizontalOffsetProperty extends OffsetProperty {
    383         public HorizontalOffsetProperty() {
    384             super("offsetLeftAndRight");
    385         }
    386 
    387         @Override
    388         protected void offsetBy(View view, int by) {
    389             view.offsetLeftAndRight(by);
    390         }
    391     }
    392 
    393     private static class VerticalOffsetProperty extends OffsetProperty {
    394         public VerticalOffsetProperty() {
    395             super("offsetTopAndBottom");
    396         }
    397 
    398         @Override
    399         protected void offsetBy(View view, int by) {
    400             view.offsetTopAndBottom(by);
    401         }
    402     }
    403 }
    404