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