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