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 com.android.internal.R;
     20 
     21 import android.animation.Animator;
     22 import android.animation.AnimatorListenerAdapter;
     23 import android.content.Context;
     24 import android.content.res.TypedArray;
     25 import android.graphics.Bitmap;
     26 import android.graphics.Canvas;
     27 import android.graphics.drawable.BitmapDrawable;
     28 import android.util.AttributeSet;
     29 import android.view.View;
     30 import android.view.ViewGroup;
     31 /**
     32  * This transition tracks changes to the visibility of target views in the
     33  * start and end scenes. Visibility is determined not just by the
     34  * {@link View#setVisibility(int)} state of views, but also whether
     35  * views exist in the current view hierarchy. The class is intended to be a
     36  * utility for subclasses such as {@link Fade}, which use this visibility
     37  * information to determine the specific animations to run when visibility
     38  * changes occur. Subclasses should implement one or both of the methods
     39  * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)},
     40  * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)} or
     41  * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)},
     42  * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}.
     43  */
     44 public abstract class Visibility extends Transition {
     45 
     46     static final String PROPNAME_VISIBILITY = "android:visibility:visibility";
     47     private static final String PROPNAME_PARENT = "android:visibility:parent";
     48     private static final String PROPNAME_SCREEN_LOCATION = "android:visibility:screenLocation";
     49 
     50     /**
     51      * Mode used in {@link #setMode(int)} to make the transition
     52      * operate on targets that are appearing. Maybe be combined with
     53      * {@link #MODE_OUT} to target Visibility changes both in and out.
     54      */
     55     public static final int MODE_IN = 0x1;
     56 
     57     /**
     58      * Mode used in {@link #setMode(int)} to make the transition
     59      * operate on targets that are disappearing. Maybe be combined with
     60      * {@link #MODE_IN} to target Visibility changes both in and out.
     61      */
     62     public static final int MODE_OUT = 0x2;
     63 
     64     private static final String[] sTransitionProperties = {
     65             PROPNAME_VISIBILITY,
     66             PROPNAME_PARENT,
     67     };
     68 
     69     private static class VisibilityInfo {
     70         boolean visibilityChange;
     71         boolean fadeIn;
     72         int startVisibility;
     73         int endVisibility;
     74         ViewGroup startParent;
     75         ViewGroup endParent;
     76     }
     77 
     78     private int mMode = MODE_IN | MODE_OUT;
     79 
     80     private int mForcedStartVisibility = -1;
     81     private int mForcedEndVisibility = -1;
     82 
     83     public Visibility() {}
     84 
     85     public Visibility(Context context, AttributeSet attrs) {
     86         super(context, attrs);
     87         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.VisibilityTransition);
     88         int mode = a.getInt(R.styleable.VisibilityTransition_transitionVisibilityMode, 0);
     89         a.recycle();
     90         if (mode != 0) {
     91             setMode(mode);
     92         }
     93     }
     94 
     95     /**
     96      * Changes the transition to support appearing and/or disappearing Views, depending
     97      * on <code>mode</code>.
     98      *
     99      * @param mode The behavior supported by this transition, a combination of
    100      *             {@link #MODE_IN} and {@link #MODE_OUT}.
    101      * @attr ref android.R.styleable#VisibilityTransition_transitionVisibilityMode
    102      */
    103     public void setMode(int mode) {
    104         if ((mode & ~(MODE_IN | MODE_OUT)) != 0) {
    105             throw new IllegalArgumentException("Only MODE_IN and MODE_OUT flags are allowed");
    106         }
    107         mMode = mode;
    108     }
    109 
    110     /**
    111      * Returns whether appearing and/or disappearing Views are supported.
    112      *
    113      * Returns whether appearing and/or disappearing Views are supported. A combination of
    114      *         {@link #MODE_IN} and {@link #MODE_OUT}.
    115      * @attr ref android.R.styleable#VisibilityTransition_transitionVisibilityMode
    116      */
    117     public int getMode() {
    118         return mMode;
    119     }
    120 
    121     @Override
    122     public String[] getTransitionProperties() {
    123         return sTransitionProperties;
    124     }
    125 
    126     private void captureValues(TransitionValues transitionValues, int forcedVisibility) {
    127         int visibility;
    128         if (forcedVisibility != -1) {
    129             visibility = forcedVisibility;
    130         } else {
    131             visibility = transitionValues.view.getVisibility();
    132         }
    133         transitionValues.values.put(PROPNAME_VISIBILITY, visibility);
    134         transitionValues.values.put(PROPNAME_PARENT, transitionValues.view.getParent());
    135         int[] loc = new int[2];
    136         transitionValues.view.getLocationOnScreen(loc);
    137         transitionValues.values.put(PROPNAME_SCREEN_LOCATION, loc);
    138     }
    139 
    140     @Override
    141     public void captureStartValues(TransitionValues transitionValues) {
    142         captureValues(transitionValues, mForcedStartVisibility);
    143     }
    144 
    145     @Override
    146     public void captureEndValues(TransitionValues transitionValues) {
    147         captureValues(transitionValues, mForcedEndVisibility);
    148     }
    149 
    150     /** @hide */
    151     @Override
    152     public void forceVisibility(int visibility, boolean isStartValue) {
    153         if (isStartValue) {
    154             mForcedStartVisibility = visibility;
    155         } else {
    156             mForcedEndVisibility = visibility;
    157         }
    158     }
    159 
    160     /**
    161      * Returns whether the view is 'visible' according to the given values
    162      * object. This is determined by testing the same properties in the values
    163      * object that are used to determine whether the object is appearing or
    164      * disappearing in the {@link
    165      * Transition#createAnimator(ViewGroup, TransitionValues, TransitionValues)}
    166      * method. This method can be called by, for example, subclasses that want
    167      * to know whether the object is visible in the same way that Visibility
    168      * determines it for the actual animation.
    169      *
    170      * @param values The TransitionValues object that holds the information by
    171      * which visibility is determined.
    172      * @return True if the view reference by <code>values</code> is visible,
    173      * false otherwise.
    174      */
    175     public boolean isVisible(TransitionValues values) {
    176         if (values == null) {
    177             return false;
    178         }
    179         int visibility = (Integer) values.values.get(PROPNAME_VISIBILITY);
    180         View parent = (View) values.values.get(PROPNAME_PARENT);
    181 
    182         return visibility == View.VISIBLE && parent != null;
    183     }
    184 
    185     private VisibilityInfo getVisibilityChangeInfo(TransitionValues startValues,
    186             TransitionValues endValues) {
    187         final VisibilityInfo visInfo = new VisibilityInfo();
    188         visInfo.visibilityChange = false;
    189         visInfo.fadeIn = false;
    190         if (startValues != null && startValues.values.containsKey(PROPNAME_VISIBILITY)) {
    191             visInfo.startVisibility = (Integer) startValues.values.get(PROPNAME_VISIBILITY);
    192             visInfo.startParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT);
    193         } else {
    194             visInfo.startVisibility = -1;
    195             visInfo.startParent = null;
    196         }
    197         if (endValues != null && endValues.values.containsKey(PROPNAME_VISIBILITY)) {
    198             visInfo.endVisibility = (Integer) endValues.values.get(PROPNAME_VISIBILITY);
    199             visInfo.endParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT);
    200         } else {
    201             visInfo.endVisibility = -1;
    202             visInfo.endParent = null;
    203         }
    204         if (startValues != null && endValues != null) {
    205             if (visInfo.startVisibility == visInfo.endVisibility &&
    206                     visInfo.startParent == visInfo.endParent) {
    207                 return visInfo;
    208             } else {
    209                 if (visInfo.startVisibility != visInfo.endVisibility) {
    210                     if (visInfo.startVisibility == View.VISIBLE) {
    211                         visInfo.fadeIn = false;
    212                         visInfo.visibilityChange = true;
    213                     } else if (visInfo.endVisibility == View.VISIBLE) {
    214                         visInfo.fadeIn = true;
    215                         visInfo.visibilityChange = true;
    216                     }
    217                     // no visibilityChange if going between INVISIBLE and GONE
    218                 } else if (visInfo.startParent != visInfo.endParent) {
    219                     if (visInfo.endParent == null) {
    220                         visInfo.fadeIn = false;
    221                         visInfo.visibilityChange = true;
    222                     } else if (visInfo.startParent == null) {
    223                         visInfo.fadeIn = true;
    224                         visInfo.visibilityChange = true;
    225                     }
    226                 }
    227             }
    228         } else if (startValues == null && visInfo.endVisibility == View.VISIBLE) {
    229             visInfo.fadeIn = true;
    230             visInfo.visibilityChange = true;
    231         } else if (endValues == null && visInfo.startVisibility == View.VISIBLE) {
    232             visInfo.fadeIn = false;
    233             visInfo.visibilityChange = true;
    234         }
    235         return visInfo;
    236     }
    237 
    238     @Override
    239     public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
    240             TransitionValues endValues) {
    241         VisibilityInfo visInfo = getVisibilityChangeInfo(startValues, endValues);
    242         if (visInfo.visibilityChange
    243                 && (visInfo.startParent != null || visInfo.endParent != null)) {
    244             if (visInfo.fadeIn) {
    245                 return onAppear(sceneRoot, startValues, visInfo.startVisibility,
    246                         endValues, visInfo.endVisibility);
    247             } else {
    248                 return onDisappear(sceneRoot, startValues, visInfo.startVisibility,
    249                         endValues, visInfo.endVisibility
    250                 );
    251             }
    252         }
    253         return null;
    254     }
    255 
    256     /**
    257      * The default implementation of this method calls
    258      * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}.
    259      * Subclasses should override this method or
    260      * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}.
    261      * if they need to create an Animator when targets appear.
    262      * The method should only be called by the Visibility class; it is
    263      * not intended to be called from external classes.
    264      *
    265      * @param sceneRoot The root of the transition hierarchy
    266      * @param startValues The target values in the start scene
    267      * @param startVisibility The target visibility in the start scene
    268      * @param endValues The target values in the end scene
    269      * @param endVisibility The target visibility in the end scene
    270      * @return An Animator to be started at the appropriate time in the
    271      * overall transition for this scene change. A null value means no animation
    272      * should be run.
    273      */
    274     public Animator onAppear(ViewGroup sceneRoot,
    275             TransitionValues startValues, int startVisibility,
    276             TransitionValues endValues, int endVisibility) {
    277         if ((mMode & MODE_IN) != MODE_IN || endValues == null) {
    278             return null;
    279         }
    280         if (startValues == null) {
    281             VisibilityInfo parentVisibilityInfo = null;
    282             View endParent = (View) endValues.view.getParent();
    283             TransitionValues startParentValues = getMatchedTransitionValues(endParent,
    284                                                                             false);
    285             TransitionValues endParentValues = getTransitionValues(endParent, false);
    286             parentVisibilityInfo =
    287                 getVisibilityChangeInfo(startParentValues, endParentValues);
    288             if (parentVisibilityInfo.visibilityChange) {
    289                 return null;
    290             }
    291         }
    292         return onAppear(sceneRoot, endValues.view, startValues, endValues);
    293     }
    294 
    295     /**
    296      * The default implementation of this method returns a null Animator. Subclasses should
    297      * override this method to make targets appear with the desired transition. The
    298      * method should only be called from
    299      * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)}.
    300      *
    301      * @param sceneRoot The root of the transition hierarchy
    302      * @param view The View to make appear. This will be in the target scene's View hierarchy and
    303      *             will be VISIBLE.
    304      * @param startValues The target values in the start scene
    305      * @param endValues The target values in the end scene
    306      * @return An Animator to be started at the appropriate time in the
    307      * overall transition for this scene change. A null value means no animation
    308      * should be run.
    309      */
    310     public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues,
    311             TransitionValues endValues) {
    312         return null;
    313     }
    314 
    315     /**
    316      * Subclasses should override this method or
    317      * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}
    318      * if they need to create an Animator when targets disappear.
    319      * The method should only be called by the Visibility class; it is
    320      * not intended to be called from external classes.
    321      * <p>
    322      * The default implementation of this method attempts to find a View to use to call
    323      * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)},
    324      * based on the situation of the View in the View hierarchy. For example,
    325      * if a View was simply removed from its parent, then the View will be added
    326      * into a {@link android.view.ViewGroupOverlay} and passed as the <code>view</code>
    327      * parameter in {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}.
    328      * If a visible View is changed to be {@link View#GONE} or {@link View#INVISIBLE},
    329      * then it can be used as the <code>view</code> and the visibility will be changed
    330      * to {@link View#VISIBLE} for the duration of the animation. However, if a View
    331      * is in a hierarchy which is also altering its visibility, the situation can be
    332      * more complicated. In general, if a view that is no longer in the hierarchy in
    333      * the end scene still has a parent (so its parent hierarchy was removed, but it
    334      * was not removed from its parent), then it will be left alone to avoid side-effects from
    335      * improperly removing it from its parent. The only exception to this is if
    336      * the previous {@link Scene} was {@link Scene#getSceneForLayout(ViewGroup, int,
    337      * android.content.Context) created from a layout resource file}, then it is considered
    338      * safe to un-parent the starting scene view in order to make it disappear.</p>
    339      *
    340      * @param sceneRoot The root of the transition hierarchy
    341      * @param startValues The target values in the start scene
    342      * @param startVisibility The target visibility in the start scene
    343      * @param endValues The target values in the end scene
    344      * @param endVisibility The target visibility in the end scene
    345      * @return An Animator to be started at the appropriate time in the
    346      * overall transition for this scene change. A null value means no animation
    347      * should be run.
    348      */
    349     public Animator onDisappear(ViewGroup sceneRoot,
    350             TransitionValues startValues, int startVisibility,
    351             TransitionValues endValues, int endVisibility) {
    352         if ((mMode & MODE_OUT) != MODE_OUT) {
    353             return null;
    354         }
    355 
    356         View startView = (startValues != null) ? startValues.view : null;
    357         View endView = (endValues != null) ? endValues.view : null;
    358         View overlayView = null;
    359         View viewToKeep = null;
    360         if (endView == null || endView.getParent() == null) {
    361             if (endView != null) {
    362                 // endView was removed from its parent - add it to the overlay
    363                 overlayView = endView;
    364             } else if (startView != null) {
    365                 // endView does not exist. Use startView only under certain
    366                 // conditions, because placing a view in an overlay necessitates
    367                 // it being removed from its current parent
    368                 if (startView.getParent() == null) {
    369                     // no parent - safe to use
    370                     overlayView = startView;
    371                 } else if (startView.getParent() instanceof View) {
    372                     View startParent = (View) startView.getParent();
    373                     TransitionValues startParentValues = getTransitionValues(startParent, true);
    374                     TransitionValues endParentValues = getMatchedTransitionValues(startParent,
    375                             true);
    376                     VisibilityInfo parentVisibilityInfo =
    377                             getVisibilityChangeInfo(startParentValues, endParentValues);
    378                     if (!parentVisibilityInfo.visibilityChange) {
    379                         overlayView = TransitionUtils.copyViewImage(sceneRoot, startView,
    380                                 startParent);
    381                     } else if (startParent.getParent() == null) {
    382                         int id = startParent.getId();
    383                         if (id != View.NO_ID && sceneRoot.findViewById(id) != null
    384                                 && mCanRemoveViews) {
    385                             // no parent, but its parent is unparented  but the parent
    386                             // hierarchy has been replaced by a new hierarchy with the same id
    387                             // and it is safe to un-parent startView
    388                             overlayView = startView;
    389                         }
    390                     }
    391                 }
    392             }
    393         } else {
    394             // visibility change
    395             if (endVisibility == View.INVISIBLE) {
    396                 viewToKeep = endView;
    397             } else {
    398                 // Becoming GONE
    399                 if (startView == endView) {
    400                     viewToKeep = endView;
    401                 } else {
    402                     overlayView = startView;
    403                 }
    404             }
    405         }
    406         final int finalVisibility = endVisibility;
    407         final ViewGroup finalSceneRoot = sceneRoot;
    408 
    409         if (overlayView != null) {
    410             // TODO: Need to do this for general case of adding to overlay
    411             int[] screenLoc = (int[]) startValues.values.get(PROPNAME_SCREEN_LOCATION);
    412             int screenX = screenLoc[0];
    413             int screenY = screenLoc[1];
    414             int[] loc = new int[2];
    415             sceneRoot.getLocationOnScreen(loc);
    416             overlayView.offsetLeftAndRight((screenX - loc[0]) - overlayView.getLeft());
    417             overlayView.offsetTopAndBottom((screenY - loc[1]) - overlayView.getTop());
    418             sceneRoot.getOverlay().add(overlayView);
    419             Animator animator = onDisappear(sceneRoot, overlayView, startValues, endValues);
    420             if (animator == null) {
    421                 sceneRoot.getOverlay().remove(overlayView);
    422             } else {
    423                 final View finalOverlayView = overlayView;
    424                 animator.addListener(new AnimatorListenerAdapter() {
    425                     @Override
    426                     public void onAnimationEnd(Animator animation) {
    427                         finalSceneRoot.getOverlay().remove(finalOverlayView);
    428                     }
    429                 });
    430             }
    431             return animator;
    432         }
    433 
    434         if (viewToKeep != null) {
    435             int originalVisibility = -1;
    436             final boolean isForcedVisibility = mForcedStartVisibility != -1 ||
    437                     mForcedEndVisibility != -1;
    438             if (!isForcedVisibility) {
    439                 originalVisibility = viewToKeep.getVisibility();
    440                 viewToKeep.setVisibility(View.VISIBLE);
    441             }
    442             Animator animator = onDisappear(sceneRoot, viewToKeep, startValues, endValues);
    443             if (animator != null) {
    444                 final View finalViewToKeep = viewToKeep;
    445                 animator.addListener(new AnimatorListenerAdapter() {
    446                     boolean mCanceled = false;
    447 
    448                     @Override
    449                     public void onAnimationPause(Animator animation) {
    450                         if (!mCanceled && !isForcedVisibility) {
    451                             finalViewToKeep.setVisibility(finalVisibility);
    452                         }
    453                     }
    454 
    455                     @Override
    456                     public void onAnimationResume(Animator animation) {
    457                         if (!mCanceled && !isForcedVisibility) {
    458                             finalViewToKeep.setVisibility(View.VISIBLE);
    459                         }
    460                     }
    461 
    462                     @Override
    463                     public void onAnimationCancel(Animator animation) {
    464                         mCanceled = true;
    465                     }
    466 
    467                     @Override
    468                     public void onAnimationEnd(Animator animation) {
    469                         if (!mCanceled) {
    470                             if (isForcedVisibility) {
    471                                 finalViewToKeep.setTransitionAlpha(0);
    472                             } else {
    473                                 finalViewToKeep.setVisibility(finalVisibility);
    474                             }
    475                         }
    476                     }
    477                 });
    478             } else if (!isForcedVisibility) {
    479                 viewToKeep.setVisibility(originalVisibility);
    480             }
    481             return animator;
    482         }
    483         return null;
    484     }
    485 
    486     @Override
    487     boolean areValuesChanged(TransitionValues oldValues, TransitionValues newValues) {
    488         if (oldValues == null && newValues == null) {
    489             return false;
    490         }
    491         if (oldValues != null && newValues != null &&
    492                 newValues.values.containsKey(PROPNAME_VISIBILITY) !=
    493                         oldValues.values.containsKey(PROPNAME_VISIBILITY)) {
    494             // The transition wasn't targeted in either the start or end, so it couldn't
    495             // have changed.
    496             return false;
    497         }
    498         VisibilityInfo changeInfo = getVisibilityChangeInfo(oldValues, newValues);
    499         return changeInfo.visibilityChange && (changeInfo.startVisibility == View.VISIBLE ||
    500                 changeInfo.endVisibility == View.VISIBLE);
    501     }
    502 
    503     /**
    504      * The default implementation of this method returns a null Animator. Subclasses should
    505      * override this method to make targets disappear with the desired transition. The
    506      * method should only be called from
    507      * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)}.
    508      *
    509      * @param sceneRoot The root of the transition hierarchy
    510      * @param view The View to make disappear. This will be in the target scene's View
    511      *             hierarchy or in an {@link android.view.ViewGroupOverlay} and will be
    512      *             VISIBLE.
    513      * @param startValues The target values in the start scene
    514      * @param endValues The target values in the end scene
    515      * @return An Animator to be started at the appropriate time in the
    516      * overall transition for this scene change. A null value means no animation
    517      * should be run.
    518      */
    519     public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues,
    520             TransitionValues endValues) {
    521         return null;
    522     }
    523 }
    524