blob: f0adc08315e90d977b92783db13e065d5ac7cb55 [file] [log] [blame]
/*
* Copyright (C) 2017 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.support.transition;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
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.os.Build;
import android.support.annotation.NonNull;
import android.support.v4.content.res.TypedArrayUtils;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Property;
import android.view.View;
import android.view.ViewGroup;
import org.xmlpull.v1.XmlPullParser;
/**
* 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 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);
}
};
/**
* Newer platforms suppress view removal at the beginning of the animation.
*/
private static final boolean SUPPORTS_VIEW_REMOVAL_SUPPRESSION = Build.VERSION.SDK_INT >= 21;
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, Styleable.CHANGE_TRANSFORM);
mUseOverlay = TypedArrayUtils.getNamedBoolean(a, (XmlPullParser) attrs,
"reparentWithOverlay", Styleable.ChangeTransform.REPARENT_WITH_OVERLAY, true);
mReparent = TypedArrayUtils.getNamedBoolean(a, (XmlPullParser) attrs,
"reparent", 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.
*/
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.
*/
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.
*/
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.
*/
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();
ViewUtils.transformMatrixToGlobal(parent, parentMatrix);
parentMatrix.preTranslate(-parent.getScrollX(), -parent.getScrollY());
transitionValues.values.put(PROPNAME_PARENT_MATRIX, parentMatrix);
transitionValues.values.put(PROPNAME_INTERMEDIATE_MATRIX,
view.getTag(R.id.transition_transform));
transitionValues.values.put(PROPNAME_INTERMEDIATE_PARENT_MATRIX,
view.getTag(R.id.parent_matrix));
}
}
@Override
public void captureStartValues(@NonNull TransitionValues transitionValues) {
captureValues(transitionValues);
if (!SUPPORTS_VIEW_REMOVAL_SUPPRESSION) {
// We still don't know if the view is removed or not, but we need to do this here, or
// the view will be actually removed, resulting in flickering at the beginning of the
// animation. We are canceling this afterwards.
((ViewGroup) transitionValues.view.getParent()).startViewTransition(
transitionValues.view);
}
}
@Override
public void captureEndValues(@NonNull TransitionValues transitionValues) {
captureValues(transitionValues);
}
@Override
public Animator createAnimator(@NonNull 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);
} else if (!SUPPORTS_VIEW_REMOVAL_SUPPRESSION) {
// We didn't need to suppress the view removal in this case. Cancel the suppression.
startParent.endViewTransition(startValues.view);
}
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 = MatrixUtils.IDENTITY_MATRIX;
}
if (endMatrix == null) {
endMatrix = MatrixUtils.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 = PropertyValuesHolderUtils.ofPointF(
TRANSLATIONS_PROPERTY, 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.setTag(R.id.transition_transform, null);
view.setTag(R.id.parent_matrix, null);
}
}
ViewUtils.setAnimationMatrix(view, 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.setTag(R.id.transition_transform, mTempMatrix);
transforms.restore(view);
}
};
animator.addListener(listener);
AnimatorUtils.addPauseListener(animator, 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);
ViewUtils.transformMatrixToLocal(sceneRoot, localEndMatrix);
GhostViewImpl ghostView = GhostViewUtils.addGhost(view, sceneRoot, localEndMatrix);
if (ghostView == null) {
return;
}
// Ask GhostView to actually remove the start view when it starts drawing the animation.
ghostView.reserveEndViewTransition((ViewGroup) startValues.values.get(PROPNAME_PARENT),
startValues.view);
Transition outerTransition = this;
while (outerTransition.mParent != null) {
outerTransition = outerTransition.mParent;
}
GhostListener listener = new GhostListener(view, ghostView);
outerTransition.addListener(listener);
// We cannot do this for older platforms or it invalidates the view and results in
// flickering, but the view will still be invisible by actually removing it from the parent.
if (SUPPORTS_VIEW_REMOVAL_SUPPRESSION) {
if (startValues.view != endValues.view) {
ViewUtils.setTransitionAlpha(startValues.view, 0);
}
ViewUtils.setTransitionAlpha(view, 1);
}
}
private void setMatricesForParent(TransitionValues startValues, TransitionValues endValues) {
Matrix endParentMatrix = (Matrix) endValues.values.get(PROPNAME_PARENT_MATRIX);
endValues.view.setTag(R.id.parent_matrix, 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);
ViewCompat.setTranslationZ(view, translationZ);
view.setScaleX(scaleX);
view.setScaleY(scaleY);
view.setRotationX(rotationX);
view.setRotationY(rotationY);
view.setRotation(rotationZ);
}
private static class Transforms {
final float mTranslationX;
final float mTranslationY;
final float mTranslationZ;
final float mScaleX;
final float mScaleY;
final float mRotationX;
final float mRotationY;
final float mRotationZ;
Transforms(View view) {
mTranslationX = view.getTranslationX();
mTranslationY = view.getTranslationY();
mTranslationZ = ViewCompat.getTranslationZ(view);
mScaleX = view.getScaleX();
mScaleY = view.getScaleY();
mRotationX = view.getRotationX();
mRotationY = view.getRotationY();
mRotationZ = view.getRotation();
}
public void restore(View view) {
setTransforms(view, mTranslationX, mTranslationY, mTranslationZ, mScaleX, mScaleY,
mRotationX, mRotationY, mRotationZ);
}
@Override
public boolean equals(Object that) {
if (!(that instanceof Transforms)) {
return false;
}
Transforms thatTransform = (Transforms) that;
return thatTransform.mTranslationX == mTranslationX
&& thatTransform.mTranslationY == mTranslationY
&& thatTransform.mTranslationZ == mTranslationZ
&& thatTransform.mScaleX == mScaleX
&& thatTransform.mScaleY == mScaleY
&& thatTransform.mRotationX == mRotationX
&& thatTransform.mRotationY == mRotationY
&& thatTransform.mRotationZ == mRotationZ;
}
@Override
public int hashCode() {
int code = mTranslationX != +0.0f ? Float.floatToIntBits(mTranslationX) : 0;
code = 31 * code + (mTranslationY != +0.0f ? Float.floatToIntBits(mTranslationY) : 0);
code = 31 * code + (mTranslationZ != +0.0f ? Float.floatToIntBits(mTranslationZ) : 0);
code = 31 * code + (mScaleX != +0.0f ? Float.floatToIntBits(mScaleX) : 0);
code = 31 * code + (mScaleY != +0.0f ? Float.floatToIntBits(mScaleY) : 0);
code = 31 * code + (mRotationX != +0.0f ? Float.floatToIntBits(mRotationX) : 0);
code = 31 * code + (mRotationY != +0.0f ? Float.floatToIntBits(mRotationY) : 0);
code = 31 * code + (mRotationZ != +0.0f ? Float.floatToIntBits(mRotationZ) : 0);
return code;
}
}
private static class GhostListener extends TransitionListenerAdapter {
private View mView;
private GhostViewImpl mGhostView;
GhostListener(View view, GhostViewImpl ghostView) {
mView = view;
mGhostView = ghostView;
}
@Override
public void onTransitionEnd(@NonNull Transition transition) {
transition.removeListener(this);
GhostViewUtils.removeGhost(mView);
mView.setTag(R.id.transition_transform, null);
mView.setTag(R.id.parent_matrix, null);
}
@Override
public void onTransitionPause(@NonNull Transition transition) {
mGhostView.setVisibility(View.INVISIBLE);
}
@Override
public void onTransitionResume(@NonNull 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;
PathAnimatorMatrix(View view, float[] values) {
mView = view;
mValues = values.clone();
mTranslationX = mValues[Matrix.MTRANS_X];
mTranslationY = mValues[Matrix.MTRANS_Y];
setAnimationMatrix();
}
void setValues(float[] values) {
System.arraycopy(values, 0, mValues, 0, values.length);
setAnimationMatrix();
}
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);
ViewUtils.setAnimationMatrix(mView, mMatrix);
}
Matrix getMatrix() {
return mMatrix;
}
}
}