content: /*
   * Copyright (C) 2014 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.FloatArrayEvaluator;
  import android.animation.ObjectAnimator;
  import android.animation.PropertyValuesHolder;
  import android.content.Context;
  import android.content.res.TypedArray;
  import android.graphics.Matrix;
  import android.graphics.Path;
  import android.graphics.PointF;
  import android.util.AttributeSet;
  import android.util.Property;
  import android.view.GhostView;
  import android.view.View;
  import android.view.ViewGroup;
  
  import com.android.internal.R;
  
  /**
   * This Transition captures scale and rotation for Views before and after the
   * scene change and animates those changes during the transition.
   *
   * A change in parent is handled as well by capturing the transforms from
   * the parent before and after the scene change and animating those during the
   * transition.
   */
  public class ChangeTransform extends Transition {
  
      private static final String TAG = "ChangeTransform";
  
      private static final String PROPNAME_MATRIX = "android:changeTransform:matrix";
      private static final String PROPNAME_TRANSFORMS = "android:changeTransform:transforms";
      private static final String PROPNAME_PARENT = "android:changeTransform:parent";
      private static final String PROPNAME_PARENT_MATRIX = "android:changeTransform:parentMatrix";
      private static final String PROPNAME_INTERMEDIATE_PARENT_MATRIX =
              "android:changeTransform:intermediateParentMatrix";
      private static final String PROPNAME_INTERMEDIATE_MATRIX =
              "android:changeTransform:intermediateMatrix";
  
      private static final String[] sTransitionProperties = {
              PROPNAME_MATRIX,
              PROPNAME_TRANSFORMS,
              PROPNAME_PARENT_MATRIX,
      };
  
      /**
       * This property sets the animation matrix properties that are not translations.
       */
      private static final Property<PathAnimatorMatrix, float[]> NON_TRANSLATIONS_PROPERTY =
              new Property<PathAnimatorMatrix, float[]>(float[].class, "nonTranslations") {
                  @Override
                  public float[] get(PathAnimatorMatrix object) {
                      return null;
                  }
  
                  @Override
                  public void set(PathAnimatorMatrix object, float[] value) {
                      object.setValues(value);
                  }
              };
  
      /**
       * This property sets the translation animation matrix properties.
       */
      private static final Property<PathAnimatorMatrix, PointF> TRANSLATIONS_PROPERTY =
              new Property<PathAnimatorMatrix, PointF>(PointF.class, "translations") {
                  @Override
                  public PointF get(PathAnimatorMatrix object) {
                      return null;
                  }
  
                  @Override
                  public void set(PathAnimatorMatrix object, PointF value) {
                      object.setTranslation(value);
                  }
              };
  
      private boolean mUseOverlay = true;
      private boolean mReparent = true;
      private Matrix mTempMatrix = new Matrix();
  
      public ChangeTransform() {}
  
      public ChangeTransform(Context context, AttributeSet attrs) {
          super(context, attrs);
          TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ChangeTransform);
          mUseOverlay = a.getBoolean(R.styleable.ChangeTransform_reparentWithOverlay, true);
          mReparent = a.getBoolean(R.styleable.ChangeTransform_reparent, true);
          a.recycle();
      }
  
      /**
       * Returns whether changes to parent should use an overlay or not. When the parent
       * change doesn't use an overlay, it affects the transforms of the child. The
       * default value is <code>true</code>.
       *
       * <p>Note: when Overlays are not used when a parent changes, a view can be clipped when
       * it moves outside the bounds of its parent. Setting
       * {@link android.view.ViewGroup#setClipChildren(boolean)} and
       * {@link android.view.ViewGroup#setClipToPadding(boolean)} can help. Also, when
       * Overlays are not used and the parent is animating its location, the position of the
       * child view will be relative to its parent's final position, so it may appear to "jump"
       * at the beginning.</p>
       *
       * @return <code>true</code> when a changed parent should execute the transition
       * inside the scene root's overlay or <code>false</code> if a parent change only
       * affects the transform of the transitioning view.
       * @attr ref android.R.styleable#ChangeTransform_reparentWithOverlay
       */
      public boolean getReparentWithOverlay() {
          return mUseOverlay;
      }
  
      /**
       * Sets whether changes to parent should use an overlay or not. When the parent
       * change doesn't use an overlay, it affects the transforms of the child. The
       * default value is <code>true</code>.
       *
       * <p>Note: when Overlays are not used when a parent changes, a view can be clipped when
       * it moves outside the bounds of its parent. Setting
       * {@link android.view.ViewGroup#setClipChildren(boolean)} and
       * {@link android.view.ViewGroup#setClipToPadding(boolean)} can help. Also, when
       * Overlays are not used and the parent is animating its location, the position of the
       * child view will be relative to its parent's final position, so it may appear to "jump"
       * at the beginning.</p>
       *
       * @param reparentWithOverlay <code>true</code> when a changed parent should execute the
       *                            transition inside the scene root's overlay or <code>false</code>
       *                            if a parent change only affects the transform of the transitioning
       *                            view.
       * @attr ref android.R.styleable#ChangeTransform_reparentWithOverlay
       */
      public void setReparentWithOverlay(boolean reparentWithOverlay) {
          mUseOverlay = reparentWithOverlay;
      }
  
      /**
       * Returns whether parent changes will be tracked by the ChangeTransform. If parent
       * changes are tracked, then the transform will adjust to the transforms of the
       * different parents. If they aren't tracked, only the transforms of the transitioning
       * view will be tracked. Default is true.
       *
       * @return whether parent changes will be tracked by the ChangeTransform.
       * @attr ref android.R.styleable#ChangeTransform_reparent
       */
      public boolean getReparent() {
          return mReparent;
      }
  
      /**
       * Sets whether parent changes will be tracked by the ChangeTransform. If parent
       * changes are tracked, then the transform will adjust to the transforms of the
       * different parents. If they aren't tracked, only the transforms of the transitioning
       * view will be tracked. Default is true.
       *
       * @param reparent Set to true to track parent changes or false to only track changes
       *                 of the transitioning view without considering the parent change.
       * @attr ref android.R.styleable#ChangeTransform_reparent
       */
      public void setReparent(boolean reparent) {
          mReparent = reparent;
      }
  
      @Override
      public String[] getTransitionProperties() {
          return sTransitionProperties;
      }
  
      private void captureValues(TransitionValues transitionValues) {
          View view = transitionValues.view;
          if (view.getVisibility() == View.GONE) {
              return;
          }
          transitionValues.values.put(PROPNAME_PARENT, view.getParent());
          Transforms transforms = new Transforms(view);
          transitionValues.values.put(PROPNAME_TRANSFORMS, transforms);
          Matrix matrix = view.getMatrix();
          if (matrix == null || matrix.isIdentity()) {
              matrix = null;
          } else {
              matrix = new Matrix(matrix);
          }
          transitionValues.values.put(PROPNAME_MATRIX, matrix);
          if (mReparent) {
              Matrix parentMatrix = new Matrix();
              ViewGroup parent = (ViewGroup) view.getParent();
              parent.transformMatrixToGlobal(parentMatrix);
              parentMatrix.preTranslate(-parent.getScrollX(), -parent.getScrollY());
              transitionValues.values.put(PROPNAME_PARENT_MATRIX, parentMatrix);
              transitionValues.values.put(PROPNAME_INTERMEDIATE_MATRIX,
                      view.getTag(R.id.transitionTransform));
              transitionValues.values.put(PROPNAME_INTERMEDIATE_PARENT_MATRIX,
                      view.getTag(R.id.parentMatrix));
          }
          return;
      }
  
      @Override
      public void captureStartValues(TransitionValues transitionValues) {
          captureValues(transitionValues);
      }
  
      @Override
      public void captureEndValues(TransitionValues transitionValues) {
          captureValues(transitionValues);
      }
  
      @Override
      public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
              TransitionValues endValues) {
          if (startValues == null || endValues == null ||
                  !startValues.values.containsKey(PROPNAME_PARENT) ||
                  !endValues.values.containsKey(PROPNAME_PARENT)) {
              return null;
          }
  
          ViewGroup startParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT);
          ViewGroup endParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT);
          boolean handleParentChange = mReparent && !parentsMatch(startParent, endParent);
  
          Matrix startMatrix = (Matrix) startValues.values.get(PROPNAME_INTERMEDIATE_MATRIX);
          if (startMatrix != null) {
              startValues.values.put(PROPNAME_MATRIX, startMatrix);
          }
  
          Matrix startParentMatrix = (Matrix)
                  startValues.values.get(PROPNAME_INTERMEDIATE_PARENT_MATRIX);
          if (startParentMatrix != null) {
              startValues.values.put(PROPNAME_PARENT_MATRIX, startParentMatrix);
          }
  
          // First handle the parent change:
          if (handleParentChange) {
              setMatricesForParent(startValues, endValues);
          }
  
          // Next handle the normal matrix transform:
          ObjectAnimator transformAnimator = createTransformAnimator(startValues, endValues,
                  handleParentChange);
  
          if (handleParentChange && transformAnimator != null && mUseOverlay) {
              createGhostView(sceneRoot, startValues, endValues);
          }
  
          return transformAnimator;
      }
  
      private ObjectAnimator createTransformAnimator(TransitionValues startValues,
              TransitionValues endValues, final boolean handleParentChange) {
          Matrix startMatrix = (Matrix) startValues.values.get(PROPNAME_MATRIX);
          Matrix endMatrix = (Matrix) endValues.values.get(PROPNAME_MATRIX);
  
          if (startMatrix == null) {
              startMatrix = Matrix.IDENTITY_MATRIX;
          }
  
          if (endMatrix == null) {
              endMatrix = Matrix.IDENTITY_MATRIX;
          }
  
          if (startMatrix.equals(endMatrix)) {
              return null;
          }
  
          final Transforms transforms = (Transforms) endValues.values.get(PROPNAME_TRANSFORMS);
  
          // clear the transform properties so that we can use the animation matrix instead
          final View view = endValues.view;
          setIdentityTransforms(view);
  
          final float[] startMatrixValues = new float[9];
          startMatrix.getValues(startMatrixValues);
          final float[] endMatrixValues = new float[9];
          endMatrix.getValues(endMatrixValues);
          final PathAnimatorMatrix pathAnimatorMatrix =
                  new PathAnimatorMatrix(view, startMatrixValues);
  
          PropertyValuesHolder valuesProperty = PropertyValuesHolder.ofObject(
                  NON_TRANSLATIONS_PROPERTY, new FloatArrayEvaluator(new float[9]),
                  startMatrixValues, endMatrixValues);
          Path path = getPathMotion().getPath(startMatrixValues[Matrix.MTRANS_X],
                  startMatrixValues[Matrix.MTRANS_Y], endMatrixValues[Matrix.MTRANS_X],
                  endMatrixValues[Matrix.MTRANS_Y]);
          PropertyValuesHolder translationProperty = PropertyValuesHolder.ofObject(
                  TRANSLATIONS_PROPERTY, null, path);
          ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(pathAnimatorMatrix,
                  valuesProperty, translationProperty);
  
          final Matrix finalEndMatrix = endMatrix;
  
          AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
              private boolean mIsCanceled;
              private Matrix mTempMatrix = new Matrix();
  
              @Override
              public void onAnimationCancel(Animator animation) {
                  mIsCanceled = true;
              }
  
              @Override
              public void onAnimationEnd(Animator animation) {
                  if (!mIsCanceled) {
                      if (handleParentChange && mUseOverlay) {
                          setCurrentMatrix(finalEndMatrix);
                      } else {
                          view.setTagInternal(R.id.transitionTransform, null);
                          view.setTagInternal(R.id.parentMatrix, null);
                      }
                  }
                  view.setAnimationMatrix(null);
                  transforms.restore(view);
              }
  
              @Override
              public void onAnimationPause(Animator animation) {
                  Matrix currentMatrix = pathAnimatorMatrix.getMatrix();
                  setCurrentMatrix(currentMatrix);
              }
  
              @Override
              public void onAnimationResume(Animator animation) {
                  setIdentityTransforms(view);
              }
  
              private void setCurrentMatrix(Matrix currentMatrix) {
                  mTempMatrix.set(currentMatrix);
                  view.setTagInternal(R.id.transitionTransform, mTempMatrix);
                  transforms.restore(view);
              }
          };
  
          animator.addListener(listener);
          animator.addPauseListener(listener);
          return animator;
      }
  
      private boolean parentsMatch(ViewGroup startParent, ViewGroup endParent) {
          boolean parentsMatch = false;
          if (!isValidTarget(startParent) || !isValidTarget(endParent)) {
              parentsMatch = startParent == endParent;
          } else {
              TransitionValues endValues = getMatchedTransitionValues(startParent, true);
              if (endValues != null) {
                  parentsMatch = endParent == endValues.view;
              }
          }
          return parentsMatch;
      }
  
      private void createGhostView(final ViewGroup sceneRoot, TransitionValues startValues,
              TransitionValues endValues) {
          View view = endValues.view;
  
          Matrix endMatrix = (Matrix) endValues.values.get(PROPNAME_PARENT_MATRIX);
          Matrix localEndMatrix = new Matrix(endMatrix);
          sceneRoot.transformMatrixToLocal(localEndMatrix);
  
          GhostView ghostView = GhostView.addGhost(view, sceneRoot, localEndMatrix);
  
          Transition outerTransition = this;
          while (outerTransition.mParent != null) {
              outerTransition = outerTransition.mParent;
          }
          GhostListener listener = new GhostListener(view, startValues.view, ghostView);
          outerTransition.addListener(listener);
  
          if (startValues.view != endValues.view) {
              startValues.view.setTransitionAlpha(0);
          }
          view.setTransitionAlpha(1);
      }
  
      private void setMatricesForParent(TransitionValues startValues, TransitionValues endValues) {
          Matrix endParentMatrix = (Matrix) endValues.values.get(PROPNAME_PARENT_MATRIX);
          endValues.view.setTagInternal(R.id.parentMatrix, endParentMatrix);
  
          Matrix toLocal = mTempMatrix;
          toLocal.reset();
          endParentMatrix.invert(toLocal);
  
          Matrix startLocal = (Matrix) startValues.values.get(PROPNAME_MATRIX);
          if (startLocal == null) {
              startLocal = new Matrix();
              startValues.values.put(PROPNAME_MATRIX, startLocal);
          }
  
          Matrix startParentMatrix = (Matrix) startValues.values.get(PROPNAME_PARENT_MATRIX);
          startLocal.postConcat(startParentMatrix);
          startLocal.postConcat(toLocal);
      }
  
      private static void setIdentityTransforms(View view) {
          setTransforms(view, 0, 0, 0, 1, 1, 0, 0, 0);
      }
  
      private static void setTransforms(View view, float translationX, float translationY,
              float translationZ, float scaleX, float scaleY, float rotationX,
              float rotationY, float rotationZ) {
          view.setTranslationX(translationX);
          view.setTranslationY(translationY);
          view.setTranslationZ(translationZ);
          view.setScaleX(scaleX);
          view.setScaleY(scaleY);
          view.setRotationX(rotationX);
          view.setRotationY(rotationY);
          view.setRotation(rotationZ);
      }
  
      private static class Transforms {
          public final float translationX;
          public final float translationY;
          public final float translationZ;
          public final float scaleX;
          public final float scaleY;
          public final float rotationX;
          public final float rotationY;
          public final float rotationZ;
  
          public Transforms(View view) {
              translationX = view.getTranslationX();
              translationY = view.getTranslationY();
              translationZ = view.getTranslationZ();
              scaleX = view.getScaleX();
              scaleY = view.getScaleY();
              rotationX = view.getRotationX();
              rotationY = view.getRotationY();
              rotationZ = view.getRotation();
          }
  
          public void restore(View view) {
              setTransforms(view, translationX, translationY, translationZ, scaleX, scaleY,
                      rotationX, rotationY, rotationZ);
          }
  
          @Override
          public boolean equals(Object that) {
              if (!(that instanceof Transforms)) {
                  return false;
              }
              Transforms thatTransform = (Transforms) that;
              return thatTransform.translationX == translationX &&
                      thatTransform.translationY == translationY &&
                      thatTransform.translationZ == translationZ &&
                      thatTransform.scaleX == scaleX &&
                      thatTransform.scaleY == scaleY &&
                      thatTransform.rotationX == rotationX &&
                      thatTransform.rotationY == rotationY &&
                      thatTransform.rotationZ == rotationZ;
          }
      }
  
      private static class GhostListener extends TransitionListenerAdapter {
          private View mView;
          private View mStartView;
          private GhostView mGhostView;
  
          public GhostListener(View view, View startView, GhostView ghostView) {
              mView = view;
              mStartView = startView;
              mGhostView = ghostView;
          }
  
          @Override
          public void onTransitionEnd(Transition transition) {
              transition.removeListener(this);
              GhostView.removeGhost(mView);
              mView.setTagInternal(R.id.transitionTransform, null);
              mView.setTagInternal(R.id.parentMatrix, null);
              mStartView.setTransitionAlpha(1);
          }
  
          @Override
          public void onTransitionPause(Transition transition) {
              mGhostView.setVisibility(View.INVISIBLE);
          }
  
          @Override
          public void onTransitionResume(Transition transition) {
              mGhostView.setVisibility(View.VISIBLE);
          }
      }
  
      /**
       * PathAnimatorMatrix allows the translations and the rest of the matrix to be set
       * separately. This allows the PathMotion to affect the translations while scale
       * and rotation are evaluated separately.
       */
      private static class PathAnimatorMatrix {
          private final Matrix mMatrix = new Matrix();
          private final View mView;
          private final float[] mValues;
          private float mTranslationX;
          private float mTranslationY;
  
          public PathAnimatorMatrix(View view, float[] values) {
              mView = view;
              mValues = values.clone();
              mTranslationX = mValues[Matrix.MTRANS_X];
              mTranslationY = mValues[Matrix.MTRANS_Y];
              setAnimationMatrix();
          }
  
          public void setValues(float[] values) {
              System.arraycopy(values, 0, mValues, 0, values.length);
              setAnimationMatrix();
          }
  
          public void setTranslation(PointF translation) {
              mTranslationX = translation.x;
              mTranslationY = translation.y;
              setAnimationMatrix();
          }
  
          private void setAnimationMatrix() {
              mValues[Matrix.MTRANS_X] = mTranslationX;
              mValues[Matrix.MTRANS_Y] = mTranslationY;
              mMatrix.setValues(mValues);
              mView.setAnimationMatrix(mMatrix);
          }
  
          public Matrix getMatrix() {
              return mMatrix;
          }
      }
  }
  