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.AnimatorListenerAdapter;
  import android.animation.AnimatorSet;
  import android.animation.ObjectAnimator;
  import android.animation.PropertyValuesHolder;
  import android.animation.RectEvaluator;
  import android.annotation.UnsupportedAppUsage;
  import android.content.Context;
  import android.content.res.TypedArray;
  import android.graphics.Bitmap;
  import android.graphics.Canvas;
  import android.graphics.Path;
  import android.graphics.PointF;
  import android.graphics.Rect;
  import android.graphics.drawable.BitmapDrawable;
  import android.graphics.drawable.Drawable;
  import android.os.Build;
  import android.util.AttributeSet;
  import android.util.Property;
  import android.view.View;
  import android.view.ViewGroup;
  
  import com.android.internal.R;
  
  import java.util.Map;
  
  /**
   * This transition captures the layout bounds of target views before and after
   * the scene change and animates those changes during the transition.
   *
   * <p>A ChangeBounds transition can be described in a resource file by using the
   * tag <code>changeBounds</code>, using its attributes of
   * {@link android.R.styleable#ChangeBounds} along with the other standard
   * attributes of {@link android.R.styleable#Transition}.</p>
   */
  public class ChangeBounds extends Transition {
  
      private static final String PROPNAME_BOUNDS = "android:changeBounds:bounds";
      private static final String PROPNAME_CLIP = "android:changeBounds:clip";
      private static final String PROPNAME_PARENT = "android:changeBounds:parent";
      private static final String PROPNAME_WINDOW_X = "android:changeBounds:windowX";
      private static final String PROPNAME_WINDOW_Y = "android:changeBounds:windowY";
      private static final String[] sTransitionProperties = {
              PROPNAME_BOUNDS,
              PROPNAME_CLIP,
              PROPNAME_PARENT,
              PROPNAME_WINDOW_X,
              PROPNAME_WINDOW_Y
      };
  
      private static final Property<Drawable, PointF> DRAWABLE_ORIGIN_PROPERTY =
              new Property<Drawable, PointF>(PointF.class, "boundsOrigin") {
                  private Rect mBounds = new Rect();
  
                  @Override
                  public void set(Drawable object, PointF value) {
                      object.copyBounds(mBounds);
                      mBounds.offsetTo(Math.round(value.x), Math.round(value.y));
                      object.setBounds(mBounds);
                  }
  
                  @Override
                  public PointF get(Drawable object) {
                      object.copyBounds(mBounds);
                      return new PointF(mBounds.left, mBounds.top);
                  }
      };
  
      private static final Property<ViewBounds, PointF> TOP_LEFT_PROPERTY =
              new Property<ViewBounds, PointF>(PointF.class, "topLeft") {
                  @Override
                  public void set(ViewBounds viewBounds, PointF topLeft) {
                      viewBounds.setTopLeft(topLeft);
                  }
  
                  @Override
                  public PointF get(ViewBounds viewBounds) {
                      return null;
                  }
              };
  
      private static final Property<ViewBounds, PointF> BOTTOM_RIGHT_PROPERTY =
              new Property<ViewBounds, PointF>(PointF.class, "bottomRight") {
                  @Override
                  public void set(ViewBounds viewBounds, PointF bottomRight) {
                      viewBounds.setBottomRight(bottomRight);
                  }
  
                  @Override
                  public PointF get(ViewBounds viewBounds) {
                      return null;
                  }
              };
  
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
      private static final Property<View, PointF> BOTTOM_RIGHT_ONLY_PROPERTY =
              new Property<View, PointF>(PointF.class, "bottomRight") {
                  @Override
                  public void set(View view, PointF bottomRight) {
                      int left = view.getLeft();
                      int top = view.getTop();
                      int right = Math.round(bottomRight.x);
                      int bottom = Math.round(bottomRight.y);
                      view.setLeftTopRightBottom(left, top, right, bottom);
                  }
  
                  @Override
                  public PointF get(View view) {
                      return null;
                  }
              };
  
      private static final Property<View, PointF> TOP_LEFT_ONLY_PROPERTY =
              new Property<View, PointF>(PointF.class, "topLeft") {
                  @Override
                  public void set(View view, PointF topLeft) {
                      int left = Math.round(topLeft.x);
                      int top = Math.round(topLeft.y);
                      int right = view.getRight();
                      int bottom = view.getBottom();
                      view.setLeftTopRightBottom(left, top, right, bottom);
                  }
  
                  @Override
                  public PointF get(View view) {
                      return null;
                  }
              };
  
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
      private static final Property<View, PointF> POSITION_PROPERTY =
              new Property<View, PointF>(PointF.class, "position") {
                  @Override
                  public void set(View view, PointF topLeft) {
                      int left = Math.round(topLeft.x);
                      int top = Math.round(topLeft.y);
                      int right = left + view.getWidth();
                      int bottom = top + view.getHeight();
                      view.setLeftTopRightBottom(left, top, right, bottom);
                  }
  
                  @Override
                  public PointF get(View view) {
                      return null;
                  }
              };
  
      int[] tempLocation = new int[2];
      boolean mResizeClip = false;
      boolean mReparent = false;
      private static final String LOG_TAG = "ChangeBounds";
  
      private static RectEvaluator sRectEvaluator = new RectEvaluator();
  
      public ChangeBounds() {}
  
      public ChangeBounds(Context context, AttributeSet attrs) {
          super(context, attrs);
  
          TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ChangeBounds);
          boolean resizeClip = a.getBoolean(R.styleable.ChangeBounds_resizeClip, false);
          a.recycle();
          setResizeClip(resizeClip);
      }
  
      @Override
      public String[] getTransitionProperties() {
          return sTransitionProperties;
      }
  
      /**
       * When <code>resizeClip</code> is true, ChangeBounds resizes the view using the clipBounds
       * instead of changing the dimensions of the view during the animation. When
       * <code>resizeClip</code> is false, ChangeBounds resizes the View by changing its dimensions.
       *
       * <p>When resizeClip is set to true, the clip bounds is modified by ChangeBounds. Therefore,
       * {@link android.transition.ChangeClipBounds} is not compatible with ChangeBounds
       * in this mode.</p>
       *
       * @param resizeClip Used to indicate whether the view bounds should be modified or the
       *                   clip bounds should be modified by ChangeBounds.
       * @see android.view.View#setClipBounds(android.graphics.Rect)
       * @attr ref android.R.styleable#ChangeBounds_resizeClip
       */
      public void setResizeClip(boolean resizeClip) {
          mResizeClip = resizeClip;
      }
  
      /**
       * Returns true when the ChangeBounds will resize by changing the clip bounds during the
       * view animation or false when bounds are changed. The default value is false.
       *
       * @return true when the ChangeBounds will resize by changing the clip bounds during the
       * view animation or false when bounds are changed. The default value is false.
       * @attr ref android.R.styleable#ChangeBounds_resizeClip
       */
      public boolean getResizeClip() {
          return mResizeClip;
      }
  
      /**
       * Setting this flag tells ChangeBounds to track the before/after parent
       * of every view using this transition. The flag is not enabled by
       * default because it requires the parent instances to be the same
       * in the two scenes or else all parents must use ids to allow
       * the transition to determine which parents are the same.
       *
       * @param reparent true if the transition should track the parent
       * container of target views and animate parent changes.
       * @deprecated Use {@link android.transition.ChangeTransform} to handle
       * transitions between different parents.
       */
      @Deprecated
      public void setReparent(boolean reparent) {
          mReparent = reparent;
      }
  
      private void captureValues(TransitionValues values) {
          View view = values.view;
  
          if (view.isLaidOut() || view.getWidth() != 0 || view.getHeight() != 0) {
              values.values.put(PROPNAME_BOUNDS, new Rect(view.getLeft(), view.getTop(),
                      view.getRight(), view.getBottom()));
              values.values.put(PROPNAME_PARENT, values.view.getParent());
              if (mReparent) {
                  values.view.getLocationInWindow(tempLocation);
                  values.values.put(PROPNAME_WINDOW_X, tempLocation[0]);
                  values.values.put(PROPNAME_WINDOW_Y, tempLocation[1]);
              }
              if (mResizeClip) {
                  values.values.put(PROPNAME_CLIP, view.getClipBounds());
              }
          }
      }
  
      @Override
      public void captureStartValues(TransitionValues transitionValues) {
          captureValues(transitionValues);
      }
  
      @Override
      public void captureEndValues(TransitionValues transitionValues) {
          captureValues(transitionValues);
      }
  
      private boolean parentMatches(View startParent, View endParent) {
          boolean parentMatches = true;
          if (mReparent) {
              TransitionValues endValues = getMatchedTransitionValues(startParent, true);
              if (endValues == null) {
                  parentMatches = startParent == endParent;
              } else {
                  parentMatches = endParent == endValues.view;
              }
          }
          return parentMatches;
      }
  
      @Override
      public Animator createAnimator(final ViewGroup sceneRoot, TransitionValues startValues,
              TransitionValues endValues) {
          if (startValues == null || endValues == null) {
              return null;
          }
          Map<String, Object> startParentVals = startValues.values;
          Map<String, Object> endParentVals = endValues.values;
          ViewGroup startParent = (ViewGroup) startParentVals.get(PROPNAME_PARENT);
          ViewGroup endParent = (ViewGroup) endParentVals.get(PROPNAME_PARENT);
          if (startParent == null || endParent == null) {
              return null;
          }
          final View view = endValues.view;
          if (parentMatches(startParent, endParent)) {
              Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS);
              Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS);
              final int startLeft = startBounds.left;
              final int endLeft = endBounds.left;
              final int startTop = startBounds.top;
              final int endTop = endBounds.top;
              final int startRight = startBounds.right;
              final int endRight = endBounds.right;
              final int startBottom = startBounds.bottom;
              final int endBottom = endBounds.bottom;
              final int startWidth = startRight - startLeft;
              final int startHeight = startBottom - startTop;
              final int endWidth = endRight - endLeft;
              final int endHeight = endBottom - endTop;
              Rect startClip = (Rect) startValues.values.get(PROPNAME_CLIP);
              Rect endClip = (Rect) endValues.values.get(PROPNAME_CLIP);
              int numChanges = 0;
              if ((startWidth != 0 && startHeight != 0) || (endWidth != 0 && endHeight != 0)) {
                  if (startLeft != endLeft || startTop != endTop) ++numChanges;
                  if (startRight != endRight || startBottom != endBottom) ++numChanges;
              }
              if ((startClip != null && !startClip.equals(endClip)) ||
                      (startClip == null && endClip != null)) {
                  ++numChanges;
              }
              if (numChanges > 0) {
                  if (view.getParent() instanceof ViewGroup) {
                      final ViewGroup parent = (ViewGroup) view.getParent();
                      parent.suppressLayout(true);
                      TransitionListener transitionListener = new TransitionListenerAdapter() {
                          boolean mCanceled = false;
  
                          @Override
                          public void onTransitionCancel(Transition transition) {
                              parent.suppressLayout(false);
                              mCanceled = true;
                          }
  
                          @Override
                          public void onTransitionEnd(Transition transition) {
                              if (!mCanceled) {
                                  parent.suppressLayout(false);
                              }
                              transition.removeListener(this);
                          }
  
                          @Override
                          public void onTransitionPause(Transition transition) {
                              parent.suppressLayout(false);
                          }
  
                          @Override
                          public void onTransitionResume(Transition transition) {
                              parent.suppressLayout(true);
                          }
                      };
                      addListener(transitionListener);
                  }
                  Animator anim;
                  if (!mResizeClip) {
                      view.setLeftTopRightBottom(startLeft, startTop, startRight, startBottom);
                      if (numChanges == 2) {
                          if (startWidth == endWidth && startHeight == endHeight) {
                              Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft,
                                      endTop);
                              anim = ObjectAnimator.ofObject(view, POSITION_PROPERTY, null,
                                      topLeftPath);
                          } else {
                              final ViewBounds viewBounds = new ViewBounds(view);
                              Path topLeftPath = getPathMotion().getPath(startLeft, startTop,
                                      endLeft, endTop);
                              ObjectAnimator topLeftAnimator = ObjectAnimator
                                      .ofObject(viewBounds, TOP_LEFT_PROPERTY, null, topLeftPath);
  
                              Path bottomRightPath = getPathMotion().getPath(startRight, startBottom,
                                      endRight, endBottom);
                              ObjectAnimator bottomRightAnimator = ObjectAnimator.ofObject(viewBounds,
                                      BOTTOM_RIGHT_PROPERTY, null, bottomRightPath);
                              AnimatorSet set = new AnimatorSet();
                              set.playTogether(topLeftAnimator, bottomRightAnimator);
                              anim = set;
                              set.addListener(new AnimatorListenerAdapter() {
                                  // We need a strong reference to viewBounds until the
                                  // animator ends.
                                  private ViewBounds mViewBounds = viewBounds;
                              });
                          }
                      } else if (startLeft != endLeft || startTop != endTop) {
                          Path topLeftPath = getPathMotion().getPath(startLeft, startTop,
                                  endLeft, endTop);
                          anim = ObjectAnimator.ofObject(view, TOP_LEFT_ONLY_PROPERTY, null,
                                  topLeftPath);
                      } else {
                          Path bottomRight = getPathMotion().getPath(startRight, startBottom,
                                  endRight, endBottom);
                          anim = ObjectAnimator.ofObject(view, BOTTOM_RIGHT_ONLY_PROPERTY, null,
                                  bottomRight);
                      }
                  } else {
                      int maxWidth = Math.max(startWidth, endWidth);
                      int maxHeight = Math.max(startHeight, endHeight);
  
                      view.setLeftTopRightBottom(startLeft, startTop, startLeft + maxWidth,
                              startTop + maxHeight);
  
                      ObjectAnimator positionAnimator = null;
                      if (startLeft != endLeft || startTop != endTop) {
                          Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft,
                                  endTop);
                          positionAnimator = ObjectAnimator.ofObject(view, POSITION_PROPERTY, null,
                                  topLeftPath);
                      }
                      final Rect finalClip = endClip;
                      if (startClip == null) {
                          startClip = new Rect(0, 0, startWidth, startHeight);
                      }
                      if (endClip == null) {
                          endClip = new Rect(0, 0, endWidth, endHeight);
                      }
                      ObjectAnimator clipAnimator = null;
                      if (!startClip.equals(endClip)) {
                          view.setClipBounds(startClip);
                          clipAnimator = ObjectAnimator.ofObject(view, "clipBounds", sRectEvaluator,
                                  startClip, endClip);
                          clipAnimator.addListener(new AnimatorListenerAdapter() {
                              private boolean mIsCanceled;
  
                              @Override
                              public void onAnimationCancel(Animator animation) {
                                  mIsCanceled = true;
                              }
  
                              @Override
                              public void onAnimationEnd(Animator animation) {
                                  if (!mIsCanceled) {
                                      view.setClipBounds(finalClip);
                                      view.setLeftTopRightBottom(endLeft, endTop, endRight,
                                              endBottom);
                                  }
                              }
                          });
                      }
                      anim = TransitionUtils.mergeAnimators(positionAnimator,
                              clipAnimator);
                  }
                  return anim;
              }
          } else {
              sceneRoot.getLocationInWindow(tempLocation);
              int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X) - tempLocation[0];
              int startY = (Integer) startValues.values.get(PROPNAME_WINDOW_Y) - tempLocation[1];
              int endX = (Integer) endValues.values.get(PROPNAME_WINDOW_X) - tempLocation[0];
              int endY = (Integer) endValues.values.get(PROPNAME_WINDOW_Y) - tempLocation[1];
              // TODO: also handle size changes: check bounds and animate size changes
              if (startX != endX || startY != endY) {
                  final int width = view.getWidth();
                  final int height = view.getHeight();
                  Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
                  Canvas canvas = new Canvas(bitmap);
                  view.draw(canvas);
                  final BitmapDrawable drawable = new BitmapDrawable(bitmap);
                  drawable.setBounds(startX, startY, startX + width, startY + height);
                  final float transitionAlpha = view.getTransitionAlpha();
                  view.setTransitionAlpha(0);
                  sceneRoot.getOverlay().add(drawable);
                  Path topLeftPath = getPathMotion().getPath(startX, startY, endX, endY);
                  PropertyValuesHolder origin = PropertyValuesHolder.ofObject(
                          DRAWABLE_ORIGIN_PROPERTY, null, topLeftPath);
                  ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, origin);
                  anim.addListener(new AnimatorListenerAdapter() {
                      @Override
                      public void onAnimationEnd(Animator animation) {
                          sceneRoot.getOverlay().remove(drawable);
                          view.setTransitionAlpha(transitionAlpha);
                      }
                  });
                  return anim;
              }
          }
          return null;
      }
  
      private static class ViewBounds {
          private int mLeft;
          private int mTop;
          private int mRight;
          private int mBottom;
          private View mView;
          private int mTopLeftCalls;
          private int mBottomRightCalls;
  
          public ViewBounds(View view) {
              mView = view;
          }
  
          public void setTopLeft(PointF topLeft) {
              mLeft = Math.round(topLeft.x);
              mTop = Math.round(topLeft.y);
              mTopLeftCalls++;
              if (mTopLeftCalls == mBottomRightCalls) {
                  setLeftTopRightBottom();
              }
          }
  
          public void setBottomRight(PointF bottomRight) {
              mRight = Math.round(bottomRight.x);
              mBottom = Math.round(bottomRight.y);
              mBottomRightCalls++;
              if (mTopLeftCalls == mBottomRightCalls) {
                  setLeftTopRightBottom();
              }
          }
  
          private void setLeftTopRightBottom() {
              mView.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
              mTopLeftCalls = 0;
              mBottomRightCalls = 0;
          }
      }
  }
  