content: /*
   * Copyright (C) 2013 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.transition;
  
  import android.animation.Animator;
  import android.animation.Animator.AnimatorListener;
  import android.animation.Animator.AnimatorPauseListener;
  import android.annotation.IntDef;
  import android.content.Context;
  import android.content.res.TypedArray;
  import android.util.AttributeSet;
  import android.view.View;
  import android.view.ViewGroup;
  import android.view.ViewGroupOverlay;
  
  import com.android.internal.R;
  
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  
  /**
   * This transition tracks changes to the visibility of target views in the
   * start and end scenes. Visibility is determined not just by the
   * {@link View#setVisibility(int)} state of views, but also whether
   * views exist in the current view hierarchy. The class is intended to be a
   * utility for subclasses such as {@link Fade}, which use this visibility
   * information to determine the specific animations to run when visibility
   * changes occur. Subclasses should implement one or both of the methods
   * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)},
   * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)} or
   * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)},
   * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}.
   */
  public abstract class Visibility extends Transition {
  
      static final String PROPNAME_VISIBILITY = "android:visibility:visibility";
      private static final String PROPNAME_PARENT = "android:visibility:parent";
      private static final String PROPNAME_SCREEN_LOCATION = "android:visibility:screenLocation";
  
      /** @hide */
      @Retention(RetentionPolicy.SOURCE)
      @IntDef(flag = true, value = {
              MODE_IN,
              MODE_OUT,
              Fade.IN,
              Fade.OUT
      })
      @interface VisibilityMode {}
  
      /**
       * Mode used in {@link #setMode(int)} to make the transition
       * operate on targets that are appearing. Maybe be combined with
       * {@link #MODE_OUT} to target Visibility changes both in and out.
       */
      public static final int MODE_IN = 0x1;
  
      /**
       * Mode used in {@link #setMode(int)} to make the transition
       * operate on targets that are disappearing. Maybe be combined with
       * {@link #MODE_IN} to target Visibility changes both in and out.
       */
      public static final int MODE_OUT = 0x2;
  
      private static final String[] sTransitionProperties = {
              PROPNAME_VISIBILITY,
              PROPNAME_PARENT,
      };
  
      private static class VisibilityInfo {
          boolean visibilityChange;
          boolean fadeIn;
          int startVisibility;
          int endVisibility;
          ViewGroup startParent;
          ViewGroup endParent;
      }
  
      private int mMode = MODE_IN | MODE_OUT;
      private boolean mSuppressLayout = true;
  
      public Visibility() {}
  
      public Visibility(Context context, AttributeSet attrs) {
          super(context, attrs);
          TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.VisibilityTransition);
          int mode = a.getInt(R.styleable.VisibilityTransition_transitionVisibilityMode, 0);
          a.recycle();
          if (mode != 0) {
              setMode(mode);
          }
      }
  
      /**
       * This tells the Visibility transition to suppress layout during the transition and release
       * the suppression after the transition.
       * @hide
       */
      public void setSuppressLayout(boolean suppress) {
          this.mSuppressLayout = suppress;
      }
  
      /**
       * Changes the transition to support appearing and/or disappearing Views, depending
       * on <code>mode</code>.
       *
       * @param mode The behavior supported by this transition, a combination of
       *             {@link #MODE_IN} and {@link #MODE_OUT}.
       * @attr ref android.R.styleable#VisibilityTransition_transitionVisibilityMode
       */
      public void setMode(@VisibilityMode int mode) {
          if ((mode & ~(MODE_IN | MODE_OUT)) != 0) {
              throw new IllegalArgumentException("Only MODE_IN and MODE_OUT flags are allowed");
          }
          mMode = mode;
      }
  
      /**
       * Returns whether appearing and/or disappearing Views are supported.
       *
       * Returns whether appearing and/or disappearing Views are supported. A combination of
       *         {@link #MODE_IN} and {@link #MODE_OUT}.
       * @attr ref android.R.styleable#VisibilityTransition_transitionVisibilityMode
       */
      @VisibilityMode
      public int getMode() {
          return mMode;
      }
  
      @Override
      public String[] getTransitionProperties() {
          return sTransitionProperties;
      }
  
      private void captureValues(TransitionValues transitionValues) {
          int visibility = transitionValues.view.getVisibility();
          transitionValues.values.put(PROPNAME_VISIBILITY, visibility);
          transitionValues.values.put(PROPNAME_PARENT, transitionValues.view.getParent());
          int[] loc = new int[2];
          transitionValues.view.getLocationOnScreen(loc);
          transitionValues.values.put(PROPNAME_SCREEN_LOCATION, loc);
      }
  
      @Override
      public void captureStartValues(TransitionValues transitionValues) {
          captureValues(transitionValues);
      }
  
      @Override
      public void captureEndValues(TransitionValues transitionValues) {
          captureValues(transitionValues);
      }
  
      /**
       * Returns whether the view is 'visible' according to the given values
       * object. This is determined by testing the same properties in the values
       * object that are used to determine whether the object is appearing or
       * disappearing in the {@link
       * Transition#createAnimator(ViewGroup, TransitionValues, TransitionValues)}
       * method. This method can be called by, for example, subclasses that want
       * to know whether the object is visible in the same way that Visibility
       * determines it for the actual animation.
       *
       * @param values The TransitionValues object that holds the information by
       * which visibility is determined.
       * @return True if the view reference by <code>values</code> is visible,
       * false otherwise.
       */
      public boolean isVisible(TransitionValues values) {
          if (values == null) {
              return false;
          }
          int visibility = (Integer) values.values.get(PROPNAME_VISIBILITY);
          View parent = (View) values.values.get(PROPNAME_PARENT);
  
          return visibility == View.VISIBLE && parent != null;
      }
  
      private static VisibilityInfo getVisibilityChangeInfo(TransitionValues startValues,
              TransitionValues endValues) {
          final VisibilityInfo visInfo = new VisibilityInfo();
          visInfo.visibilityChange = false;
          visInfo.fadeIn = false;
          if (startValues != null && startValues.values.containsKey(PROPNAME_VISIBILITY)) {
              visInfo.startVisibility = (Integer) startValues.values.get(PROPNAME_VISIBILITY);
              visInfo.startParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT);
          } else {
              visInfo.startVisibility = -1;
              visInfo.startParent = null;
          }
          if (endValues != null && endValues.values.containsKey(PROPNAME_VISIBILITY)) {
              visInfo.endVisibility = (Integer) endValues.values.get(PROPNAME_VISIBILITY);
              visInfo.endParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT);
          } else {
              visInfo.endVisibility = -1;
              visInfo.endParent = null;
          }
          if (startValues != null && endValues != null) {
              if (visInfo.startVisibility == visInfo.endVisibility &&
                      visInfo.startParent == visInfo.endParent) {
                  return visInfo;
              } else {
                  if (visInfo.startVisibility != visInfo.endVisibility) {
                      if (visInfo.startVisibility == View.VISIBLE) {
                          visInfo.fadeIn = false;
                          visInfo.visibilityChange = true;
                      } else if (visInfo.endVisibility == View.VISIBLE) {
                          visInfo.fadeIn = true;
                          visInfo.visibilityChange = true;
                      }
                      // no visibilityChange if going between INVISIBLE and GONE
                  } else if (visInfo.startParent != visInfo.endParent) {
                      if (visInfo.endParent == null) {
                          visInfo.fadeIn = false;
                          visInfo.visibilityChange = true;
                      } else if (visInfo.startParent == null) {
                          visInfo.fadeIn = true;
                          visInfo.visibilityChange = true;
                      }
                  }
              }
          } else if (startValues == null && visInfo.endVisibility == View.VISIBLE) {
              visInfo.fadeIn = true;
              visInfo.visibilityChange = true;
          } else if (endValues == null && visInfo.startVisibility == View.VISIBLE) {
              visInfo.fadeIn = false;
              visInfo.visibilityChange = true;
          }
          return visInfo;
      }
  
      @Override
      public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
              TransitionValues endValues) {
          VisibilityInfo visInfo = getVisibilityChangeInfo(startValues, endValues);
          if (visInfo.visibilityChange
                  && (visInfo.startParent != null || visInfo.endParent != null)) {
              if (visInfo.fadeIn) {
                  return onAppear(sceneRoot, startValues, visInfo.startVisibility,
                          endValues, visInfo.endVisibility);
              } else {
                  return onDisappear(sceneRoot, startValues, visInfo.startVisibility,
                          endValues, visInfo.endVisibility
                  );
              }
          }
          return null;
      }
  
      /**
       * The default implementation of this method calls
       * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}.
       * Subclasses should override this method or
       * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}.
       * if they need to create an Animator when targets appear.
       * The method should only be called by the Visibility class; it is
       * not intended to be called from external classes.
       *
       * @param sceneRoot The root of the transition hierarchy
       * @param startValues The target values in the start scene
       * @param startVisibility The target visibility in the start scene
       * @param endValues The target values in the end scene
       * @param endVisibility The target visibility in the end scene
       * @return An Animator to be started at the appropriate time in the
       * overall transition for this scene change. A null value means no animation
       * should be run.
       */
      public Animator onAppear(ViewGroup sceneRoot,
              TransitionValues startValues, int startVisibility,
              TransitionValues endValues, int endVisibility) {
          if ((mMode & MODE_IN) != MODE_IN || endValues == null) {
              return null;
          }
          if (startValues == null) {
              VisibilityInfo parentVisibilityInfo = null;
              View endParent = (View) endValues.view.getParent();
              TransitionValues startParentValues = getMatchedTransitionValues(endParent,
                                                                              false);
              TransitionValues endParentValues = getTransitionValues(endParent, false);
              parentVisibilityInfo =
                  getVisibilityChangeInfo(startParentValues, endParentValues);
              if (parentVisibilityInfo.visibilityChange) {
                  return null;
              }
          }
          return onAppear(sceneRoot, endValues.view, startValues, endValues);
      }
  
      /**
       * The default implementation of this method returns a null Animator. Subclasses should
       * override this method to make targets appear with the desired transition. The
       * method should only be called from
       * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)}.
       *
       * @param sceneRoot The root of the transition hierarchy
       * @param view The View to make appear. This will be in the target scene's View hierarchy and
       *             will be VISIBLE.
       * @param startValues The target values in the start scene
       * @param endValues The target values in the end scene
       * @return An Animator to be started at the appropriate time in the
       * overall transition for this scene change. A null value means no animation
       * should be run.
       */
      public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues,
              TransitionValues endValues) {
          return null;
      }
  
      /**
       * Subclasses should override this method or
       * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}
       * if they need to create an Animator when targets disappear.
       * The method should only be called by the Visibility class; it is
       * not intended to be called from external classes.
       * <p>
       * The default implementation of this method attempts to find a View to use to call
       * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)},
       * based on the situation of the View in the View hierarchy. For example,
       * if a View was simply removed from its parent, then the View will be added
       * into a {@link android.view.ViewGroupOverlay} and passed as the <code>view</code>
       * parameter in {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}.
       * If a visible View is changed to be {@link View#GONE} or {@link View#INVISIBLE},
       * then it can be used as the <code>view</code> and the visibility will be changed
       * to {@link View#VISIBLE} for the duration of the animation. However, if a View
       * is in a hierarchy which is also altering its visibility, the situation can be
       * more complicated. In general, if a view that is no longer in the hierarchy in
       * the end scene still has a parent (so its parent hierarchy was removed, but it
       * was not removed from its parent), then it will be left alone to avoid side-effects from
       * improperly removing it from its parent. The only exception to this is if
       * the previous {@link Scene} was {@link Scene#getSceneForLayout(ViewGroup, int,
       * android.content.Context) created from a layout resource file}, then it is considered
       * safe to un-parent the starting scene view in order to make it disappear.</p>
       *
       * @param sceneRoot The root of the transition hierarchy
       * @param startValues The target values in the start scene
       * @param startVisibility The target visibility in the start scene
       * @param endValues The target values in the end scene
       * @param endVisibility The target visibility in the end scene
       * @return An Animator to be started at the appropriate time in the
       * overall transition for this scene change. A null value means no animation
       * should be run.
       */
      public Animator onDisappear(ViewGroup sceneRoot,
              TransitionValues startValues, int startVisibility,
              TransitionValues endValues, int endVisibility) {
          if ((mMode & MODE_OUT) != MODE_OUT) {
              return null;
          }
  
          if (startValues == null) {
              // startValues(and startView) will never be null for disappear transition.
              return null;
          }
  
          final View startView = startValues.view;
          final View endView = (endValues != null) ? endValues.view : null;
          View overlayView = null;
          View viewToKeep = null;
          boolean reusingOverlayView = false;
  
          View savedOverlayView = (View) startView.getTag(R.id.transition_overlay_view_tag);
          if (savedOverlayView != null) {
              // we've already created overlay for the start view.
              // it means that we are applying two visibility
              // transitions for the same view
              overlayView = savedOverlayView;
              reusingOverlayView = true;
          } else {
              boolean needOverlayForStartView = false;
  
              if (endView == null || endView.getParent() == null) {
                  if (endView != null) {
                      // endView was removed from its parent - add it to the overlay
                      overlayView = endView;
                  } else {
                      needOverlayForStartView = true;
                  }
              } else {
                  // visibility change
                  if (endVisibility == View.INVISIBLE) {
                      viewToKeep = endView;
                  } else {
                      // Becoming GONE
                      if (startView == endView) {
                          viewToKeep = endView;
                      } else {
                          needOverlayForStartView = true;
                      }
                  }
              }
  
              if (needOverlayForStartView) {
                  // endView does not exist. Use startView only under certain
                  // conditions, because placing a view in an overlay necessitates
                  // it being removed from its current parent
                  if (startView.getParent() == null) {
                      // no parent - safe to use
                      overlayView = startView;
                  } else if (startView.getParent() instanceof View) {
                      View startParent = (View) startView.getParent();
                      TransitionValues startParentValues = getTransitionValues(startParent, true);
                      TransitionValues endParentValues = getMatchedTransitionValues(startParent,
                              true);
                      VisibilityInfo parentVisibilityInfo =
                              getVisibilityChangeInfo(startParentValues, endParentValues);
                      if (!parentVisibilityInfo.visibilityChange) {
                          overlayView = TransitionUtils.copyViewImage(sceneRoot, startView,
                                  startParent);
                      } else {
                          int id = startParent.getId();
                          if (startParent.getParent() == null && id != View.NO_ID
                                  && sceneRoot.findViewById(id) != null && mCanRemoveViews) {
                              // no parent, but its parent is unparented  but the parent
                              // hierarchy has been replaced by a new hierarchy with the same id
                              // and it is safe to un-parent startView
                              overlayView = startView;
                          } else {
                              // TODO: Handle this case as well
                          }
                      }
                  }
              }
          }
  
          if (overlayView != null) {
              // TODO: Need to do this for general case of adding to overlay
              final ViewGroupOverlay overlay;
              if (!reusingOverlayView) {
                  overlay = sceneRoot.getOverlay();
                  int[] screenLoc = (int[]) startValues.values.get(PROPNAME_SCREEN_LOCATION);
                  int screenX = screenLoc[0];
                  int screenY = screenLoc[1];
                  int[] loc = new int[2];
                  sceneRoot.getLocationOnScreen(loc);
                  overlayView.offsetLeftAndRight((screenX - loc[0]) - overlayView.getLeft());
                  overlayView.offsetTopAndBottom((screenY - loc[1]) - overlayView.getTop());
                  overlay.add(overlayView);
              } else {
                  overlay = null;
              }
              Animator animator = onDisappear(sceneRoot, overlayView, startValues, endValues);
              if (!reusingOverlayView) {
                  if (animator == null) {
                      overlay.remove(overlayView);
                  } else {
                      startView.setTagInternal(R.id.transition_overlay_view_tag, overlayView);
                      final View finalOverlayView = overlayView;
                      addListener(new TransitionListenerAdapter() {
  
                          @Override
                          public void onTransitionPause(Transition transition) {
                              overlay.remove(finalOverlayView);
                          }
  
                          @Override
                          public void onTransitionResume(Transition transition) {
                              if (finalOverlayView.getParent() == null) {
                                  overlay.add(finalOverlayView);
                              } else {
                                  cancel();
                              }
                          }
  
                          @Override
                          public void onTransitionEnd(Transition transition) {
                              startView.setTagInternal(R.id.transition_overlay_view_tag, null);
                              overlay.remove(finalOverlayView);
                              transition.removeListener(this);
                          }
                      });
                  }
              }
              return animator;
          }
  
          if (viewToKeep != null) {
              int originalVisibility = viewToKeep.getVisibility();
              viewToKeep.setTransitionVisibility(View.VISIBLE);
              Animator animator = onDisappear(sceneRoot, viewToKeep, startValues, endValues);
              if (animator != null) {
                  DisappearListener disappearListener = new DisappearListener(viewToKeep,
                          endVisibility, mSuppressLayout);
                  animator.addListener(disappearListener);
                  animator.addPauseListener(disappearListener);
                  addListener(disappearListener);
              } else {
                  viewToKeep.setTransitionVisibility(originalVisibility);
              }
              return animator;
          }
          return null;
      }
  
      @Override
      public boolean isTransitionRequired(TransitionValues startValues, TransitionValues newValues) {
          if (startValues == null && newValues == null) {
              return false;
          }
          if (startValues != null && newValues != null &&
                  newValues.values.containsKey(PROPNAME_VISIBILITY) !=
                          startValues.values.containsKey(PROPNAME_VISIBILITY)) {
              // The transition wasn't targeted in either the start or end, so it couldn't
              // have changed.
              return false;
          }
          VisibilityInfo changeInfo = getVisibilityChangeInfo(startValues, newValues);
          return changeInfo.visibilityChange && (changeInfo.startVisibility == View.VISIBLE ||
                  changeInfo.endVisibility == View.VISIBLE);
      }
  
      /**
       * The default implementation of this method returns a null Animator. Subclasses should
       * override this method to make targets disappear with the desired transition. The
       * method should only be called from
       * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)}.
       *
       * @param sceneRoot The root of the transition hierarchy
       * @param view The View to make disappear. This will be in the target scene's View
       *             hierarchy or in an {@link android.view.ViewGroupOverlay} and will be
       *             VISIBLE.
       * @param startValues The target values in the start scene
       * @param endValues The target values in the end scene
       * @return An Animator to be started at the appropriate time in the
       * overall transition for this scene change. A null value means no animation
       * should be run.
       */
      public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues,
              TransitionValues endValues) {
          return null;
      }
  
      private static class DisappearListener
              extends TransitionListenerAdapter implements AnimatorListener, AnimatorPauseListener {
          private final View mView;
          private final int mFinalVisibility;
          private final ViewGroup mParent;
          private final boolean mSuppressLayout;
  
          private boolean mLayoutSuppressed;
          boolean mCanceled = false;
  
          public DisappearListener(View view, int finalVisibility, boolean suppressLayout) {
              this.mView = view;
              this.mFinalVisibility = finalVisibility;
              this.mParent = (ViewGroup) view.getParent();
              this.mSuppressLayout = suppressLayout;
              // Prevent a layout from including mView in its calculation.
              suppressLayout(true);
          }
  
          @Override
          public void onAnimationPause(Animator animation) {
              if (!mCanceled) {
                  mView.setTransitionVisibility(mFinalVisibility);
              }
          }
  
          @Override
          public void onAnimationResume(Animator animation) {
              if (!mCanceled) {
                  mView.setTransitionVisibility(View.VISIBLE);
              }
          }
  
          @Override
          public void onAnimationCancel(Animator animation) {
              mCanceled = true;
          }
  
          @Override
          public void onAnimationRepeat(Animator animation) {
          }
  
          @Override
          public void onAnimationStart(Animator animation) {
          }
  
          @Override
          public void onAnimationEnd(Animator animation) {
              hideViewWhenNotCanceled();
          }
  
          @Override
          public void onTransitionEnd(Transition transition) {
              hideViewWhenNotCanceled();
              transition.removeListener(this);
          }
  
          @Override
          public void onTransitionPause(Transition transition) {
              suppressLayout(false);
          }
  
          @Override
          public void onTransitionResume(Transition transition) {
              suppressLayout(true);
          }
  
          private void hideViewWhenNotCanceled() {
              if (!mCanceled) {
                  // Recreate the parent's display list in case it includes mView.
                  mView.setTransitionVisibility(mFinalVisibility);
                  if (mParent != null) {
                      mParent.invalidate();
                  }
              }
              // Layout is allowed now that the View is in its final state
              suppressLayout(false);
          }
  
          private void suppressLayout(boolean suppress) {
              if (mSuppressLayout && mLayoutSuppressed != suppress && mParent != null) {
                  mLayoutSuppressed = suppress;
                  mParent.suppressLayout(suppress);
              }
          }
      }
  }
  