1/* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.transition; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ObjectAnimator; 22import android.animation.PropertyValuesHolder; 23import android.content.Context; 24import android.content.res.TypedArray; 25import android.graphics.Matrix; 26import android.graphics.Path; 27import android.graphics.PointF; 28import android.os.Build; 29import android.support.annotation.NonNull; 30import android.support.v4.content.res.TypedArrayUtils; 31import android.support.v4.view.ViewCompat; 32import android.util.AttributeSet; 33import android.util.Property; 34import android.view.View; 35import android.view.ViewGroup; 36 37import org.xmlpull.v1.XmlPullParser; 38 39/** 40 * This Transition captures scale and rotation for Views before and after the 41 * scene change and animates those changes during the transition. 42 * 43 * A change in parent is handled as well by capturing the transforms from 44 * the parent before and after the scene change and animating those during the 45 * transition. 46 */ 47public class ChangeTransform extends Transition { 48 49 private static final String PROPNAME_MATRIX = "android:changeTransform:matrix"; 50 private static final String PROPNAME_TRANSFORMS = "android:changeTransform:transforms"; 51 private static final String PROPNAME_PARENT = "android:changeTransform:parent"; 52 private static final String PROPNAME_PARENT_MATRIX = "android:changeTransform:parentMatrix"; 53 private static final String PROPNAME_INTERMEDIATE_PARENT_MATRIX = 54 "android:changeTransform:intermediateParentMatrix"; 55 private static final String PROPNAME_INTERMEDIATE_MATRIX = 56 "android:changeTransform:intermediateMatrix"; 57 58 private static final String[] sTransitionProperties = { 59 PROPNAME_MATRIX, 60 PROPNAME_TRANSFORMS, 61 PROPNAME_PARENT_MATRIX, 62 }; 63 64 /** 65 * This property sets the animation matrix properties that are not translations. 66 */ 67 private static final Property<PathAnimatorMatrix, float[]> NON_TRANSLATIONS_PROPERTY = 68 new Property<PathAnimatorMatrix, float[]>(float[].class, "nonTranslations") { 69 @Override 70 public float[] get(PathAnimatorMatrix object) { 71 return null; 72 } 73 74 @Override 75 public void set(PathAnimatorMatrix object, float[] value) { 76 object.setValues(value); 77 } 78 }; 79 80 /** 81 * This property sets the translation animation matrix properties. 82 */ 83 private static final Property<PathAnimatorMatrix, PointF> TRANSLATIONS_PROPERTY = 84 new Property<PathAnimatorMatrix, PointF>(PointF.class, "translations") { 85 @Override 86 public PointF get(PathAnimatorMatrix object) { 87 return null; 88 } 89 90 @Override 91 public void set(PathAnimatorMatrix object, PointF value) { 92 object.setTranslation(value); 93 } 94 }; 95 96 /** 97 * Newer platforms suppress view removal at the beginning of the animation. 98 */ 99 private static final boolean SUPPORTS_VIEW_REMOVAL_SUPPRESSION = Build.VERSION.SDK_INT >= 21; 100 101 private boolean mUseOverlay = true; 102 private boolean mReparent = true; 103 private Matrix mTempMatrix = new Matrix(); 104 105 public ChangeTransform() { 106 } 107 108 public ChangeTransform(Context context, AttributeSet attrs) { 109 super(context, attrs); 110 TypedArray a = context.obtainStyledAttributes(attrs, Styleable.CHANGE_TRANSFORM); 111 mUseOverlay = TypedArrayUtils.getNamedBoolean(a, (XmlPullParser) attrs, 112 "reparentWithOverlay", Styleable.ChangeTransform.REPARENT_WITH_OVERLAY, true); 113 mReparent = TypedArrayUtils.getNamedBoolean(a, (XmlPullParser) attrs, 114 "reparent", Styleable.ChangeTransform.REPARENT, true); 115 a.recycle(); 116 } 117 118 /** 119 * Returns whether changes to parent should use an overlay or not. When the parent 120 * change doesn't use an overlay, it affects the transforms of the child. The 121 * default value is <code>true</code>. 122 * 123 * <p>Note: when Overlays are not used when a parent changes, a view can be clipped when 124 * it moves outside the bounds of its parent. Setting 125 * {@link android.view.ViewGroup#setClipChildren(boolean)} and 126 * {@link android.view.ViewGroup#setClipToPadding(boolean)} can help. Also, when 127 * Overlays are not used and the parent is animating its location, the position of the 128 * child view will be relative to its parent's final position, so it may appear to "jump" 129 * at the beginning.</p> 130 * 131 * @return <code>true</code> when a changed parent should execute the transition 132 * inside the scene root's overlay or <code>false</code> if a parent change only 133 * affects the transform of the transitioning view. 134 */ 135 public boolean getReparentWithOverlay() { 136 return mUseOverlay; 137 } 138 139 /** 140 * Sets whether changes to parent should use an overlay or not. When the parent 141 * change doesn't use an overlay, it affects the transforms of the child. The 142 * default value is <code>true</code>. 143 * 144 * <p>Note: when Overlays are not used when a parent changes, a view can be clipped when 145 * it moves outside the bounds of its parent. Setting 146 * {@link android.view.ViewGroup#setClipChildren(boolean)} and 147 * {@link android.view.ViewGroup#setClipToPadding(boolean)} can help. Also, when 148 * Overlays are not used and the parent is animating its location, the position of the 149 * child view will be relative to its parent's final position, so it may appear to "jump" 150 * at the beginning.</p> 151 * 152 * @param reparentWithOverlay <code>true</code> when a changed parent should execute the 153 * transition inside the scene root's overlay or <code>false</code> 154 * if a parent change only affects the transform of the 155 * transitioning view. 156 */ 157 public void setReparentWithOverlay(boolean reparentWithOverlay) { 158 mUseOverlay = reparentWithOverlay; 159 } 160 161 /** 162 * Returns whether parent changes will be tracked by the ChangeTransform. If parent 163 * changes are tracked, then the transform will adjust to the transforms of the 164 * different parents. If they aren't tracked, only the transforms of the transitioning 165 * view will be tracked. Default is true. 166 * 167 * @return whether parent changes will be tracked by the ChangeTransform. 168 */ 169 public boolean getReparent() { 170 return mReparent; 171 } 172 173 /** 174 * Sets whether parent changes will be tracked by the ChangeTransform. If parent 175 * changes are tracked, then the transform will adjust to the transforms of the 176 * different parents. If they aren't tracked, only the transforms of the transitioning 177 * view will be tracked. Default is true. 178 * 179 * @param reparent Set to true to track parent changes or false to only track changes 180 * of the transitioning view without considering the parent change. 181 */ 182 public void setReparent(boolean reparent) { 183 mReparent = reparent; 184 } 185 186 @Override 187 public String[] getTransitionProperties() { 188 return sTransitionProperties; 189 } 190 191 private void captureValues(TransitionValues transitionValues) { 192 View view = transitionValues.view; 193 if (view.getVisibility() == View.GONE) { 194 return; 195 } 196 transitionValues.values.put(PROPNAME_PARENT, view.getParent()); 197 Transforms transforms = new Transforms(view); 198 transitionValues.values.put(PROPNAME_TRANSFORMS, transforms); 199 Matrix matrix = view.getMatrix(); 200 if (matrix == null || matrix.isIdentity()) { 201 matrix = null; 202 } else { 203 matrix = new Matrix(matrix); 204 } 205 transitionValues.values.put(PROPNAME_MATRIX, matrix); 206 if (mReparent) { 207 Matrix parentMatrix = new Matrix(); 208 ViewGroup parent = (ViewGroup) view.getParent(); 209 ViewUtils.transformMatrixToGlobal(parent, parentMatrix); 210 parentMatrix.preTranslate(-parent.getScrollX(), -parent.getScrollY()); 211 transitionValues.values.put(PROPNAME_PARENT_MATRIX, parentMatrix); 212 transitionValues.values.put(PROPNAME_INTERMEDIATE_MATRIX, 213 view.getTag(R.id.transition_transform)); 214 transitionValues.values.put(PROPNAME_INTERMEDIATE_PARENT_MATRIX, 215 view.getTag(R.id.parent_matrix)); 216 } 217 } 218 219 @Override 220 public void captureStartValues(@NonNull TransitionValues transitionValues) { 221 captureValues(transitionValues); 222 if (!SUPPORTS_VIEW_REMOVAL_SUPPRESSION) { 223 // We still don't know if the view is removed or not, but we need to do this here, or 224 // the view will be actually removed, resulting in flickering at the beginning of the 225 // animation. We are canceling this afterwards. 226 ((ViewGroup) transitionValues.view.getParent()).startViewTransition( 227 transitionValues.view); 228 } 229 } 230 231 @Override 232 public void captureEndValues(@NonNull TransitionValues transitionValues) { 233 captureValues(transitionValues); 234 } 235 236 @Override 237 public Animator createAnimator(@NonNull ViewGroup sceneRoot, TransitionValues startValues, 238 TransitionValues endValues) { 239 if (startValues == null || endValues == null 240 || !startValues.values.containsKey(PROPNAME_PARENT) 241 || !endValues.values.containsKey(PROPNAME_PARENT)) { 242 return null; 243 } 244 245 ViewGroup startParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT); 246 ViewGroup endParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT); 247 boolean handleParentChange = mReparent && !parentsMatch(startParent, endParent); 248 249 Matrix startMatrix = (Matrix) startValues.values.get(PROPNAME_INTERMEDIATE_MATRIX); 250 if (startMatrix != null) { 251 startValues.values.put(PROPNAME_MATRIX, startMatrix); 252 } 253 254 Matrix startParentMatrix = (Matrix) 255 startValues.values.get(PROPNAME_INTERMEDIATE_PARENT_MATRIX); 256 if (startParentMatrix != null) { 257 startValues.values.put(PROPNAME_PARENT_MATRIX, startParentMatrix); 258 } 259 260 // First handle the parent change: 261 if (handleParentChange) { 262 setMatricesForParent(startValues, endValues); 263 } 264 265 // Next handle the normal matrix transform: 266 ObjectAnimator transformAnimator = createTransformAnimator(startValues, endValues, 267 handleParentChange); 268 269 if (handleParentChange && transformAnimator != null && mUseOverlay) { 270 createGhostView(sceneRoot, startValues, endValues); 271 } else if (!SUPPORTS_VIEW_REMOVAL_SUPPRESSION) { 272 // We didn't need to suppress the view removal in this case. Cancel the suppression. 273 startParent.endViewTransition(startValues.view); 274 } 275 276 return transformAnimator; 277 } 278 279 private ObjectAnimator createTransformAnimator(TransitionValues startValues, 280 TransitionValues endValues, final boolean handleParentChange) { 281 Matrix startMatrix = (Matrix) startValues.values.get(PROPNAME_MATRIX); 282 Matrix endMatrix = (Matrix) endValues.values.get(PROPNAME_MATRIX); 283 284 if (startMatrix == null) { 285 startMatrix = MatrixUtils.IDENTITY_MATRIX; 286 } 287 288 if (endMatrix == null) { 289 endMatrix = MatrixUtils.IDENTITY_MATRIX; 290 } 291 292 if (startMatrix.equals(endMatrix)) { 293 return null; 294 } 295 296 final Transforms transforms = (Transforms) endValues.values.get(PROPNAME_TRANSFORMS); 297 298 // clear the transform properties so that we can use the animation matrix instead 299 final View view = endValues.view; 300 setIdentityTransforms(view); 301 302 final float[] startMatrixValues = new float[9]; 303 startMatrix.getValues(startMatrixValues); 304 final float[] endMatrixValues = new float[9]; 305 endMatrix.getValues(endMatrixValues); 306 final PathAnimatorMatrix pathAnimatorMatrix = 307 new PathAnimatorMatrix(view, startMatrixValues); 308 309 PropertyValuesHolder valuesProperty = PropertyValuesHolder.ofObject( 310 NON_TRANSLATIONS_PROPERTY, new FloatArrayEvaluator(new float[9]), 311 startMatrixValues, endMatrixValues); 312 Path path = getPathMotion().getPath(startMatrixValues[Matrix.MTRANS_X], 313 startMatrixValues[Matrix.MTRANS_Y], endMatrixValues[Matrix.MTRANS_X], 314 endMatrixValues[Matrix.MTRANS_Y]); 315 PropertyValuesHolder translationProperty = PropertyValuesHolderUtils.ofPointF( 316 TRANSLATIONS_PROPERTY, path); 317 ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(pathAnimatorMatrix, 318 valuesProperty, translationProperty); 319 320 final Matrix finalEndMatrix = endMatrix; 321 322 AnimatorListenerAdapter listener = new AnimatorListenerAdapter() { 323 private boolean mIsCanceled; 324 private Matrix mTempMatrix = new Matrix(); 325 326 @Override 327 public void onAnimationCancel(Animator animation) { 328 mIsCanceled = true; 329 } 330 331 @Override 332 public void onAnimationEnd(Animator animation) { 333 if (!mIsCanceled) { 334 if (handleParentChange && mUseOverlay) { 335 setCurrentMatrix(finalEndMatrix); 336 } else { 337 view.setTag(R.id.transition_transform, null); 338 view.setTag(R.id.parent_matrix, null); 339 } 340 } 341 ViewUtils.setAnimationMatrix(view, null); 342 transforms.restore(view); 343 } 344 345 @Override 346 public void onAnimationPause(Animator animation) { 347 Matrix currentMatrix = pathAnimatorMatrix.getMatrix(); 348 setCurrentMatrix(currentMatrix); 349 } 350 351 @Override 352 public void onAnimationResume(Animator animation) { 353 setIdentityTransforms(view); 354 } 355 356 private void setCurrentMatrix(Matrix currentMatrix) { 357 mTempMatrix.set(currentMatrix); 358 view.setTag(R.id.transition_transform, mTempMatrix); 359 transforms.restore(view); 360 } 361 }; 362 363 animator.addListener(listener); 364 AnimatorUtils.addPauseListener(animator, listener); 365 return animator; 366 } 367 368 private boolean parentsMatch(ViewGroup startParent, ViewGroup endParent) { 369 boolean parentsMatch = false; 370 if (!isValidTarget(startParent) || !isValidTarget(endParent)) { 371 parentsMatch = startParent == endParent; 372 } else { 373 TransitionValues endValues = getMatchedTransitionValues(startParent, true); 374 if (endValues != null) { 375 parentsMatch = endParent == endValues.view; 376 } 377 } 378 return parentsMatch; 379 } 380 381 private void createGhostView(final ViewGroup sceneRoot, TransitionValues startValues, 382 TransitionValues endValues) { 383 View view = endValues.view; 384 385 Matrix endMatrix = (Matrix) endValues.values.get(PROPNAME_PARENT_MATRIX); 386 Matrix localEndMatrix = new Matrix(endMatrix); 387 ViewUtils.transformMatrixToLocal(sceneRoot, localEndMatrix); 388 389 GhostViewImpl ghostView = GhostViewUtils.addGhost(view, sceneRoot, localEndMatrix); 390 if (ghostView == null) { 391 return; 392 } 393 // Ask GhostView to actually remove the start view when it starts drawing the animation. 394 ghostView.reserveEndViewTransition((ViewGroup) startValues.values.get(PROPNAME_PARENT), 395 startValues.view); 396 397 Transition outerTransition = this; 398 while (outerTransition.mParent != null) { 399 outerTransition = outerTransition.mParent; 400 } 401 402 GhostListener listener = new GhostListener(view, ghostView); 403 outerTransition.addListener(listener); 404 405 // We cannot do this for older platforms or it invalidates the view and results in 406 // flickering, but the view will still be invisible by actually removing it from the parent. 407 if (SUPPORTS_VIEW_REMOVAL_SUPPRESSION) { 408 if (startValues.view != endValues.view) { 409 ViewUtils.setTransitionAlpha(startValues.view, 0); 410 } 411 ViewUtils.setTransitionAlpha(view, 1); 412 } 413 } 414 415 private void setMatricesForParent(TransitionValues startValues, TransitionValues endValues) { 416 Matrix endParentMatrix = (Matrix) endValues.values.get(PROPNAME_PARENT_MATRIX); 417 endValues.view.setTag(R.id.parent_matrix, endParentMatrix); 418 419 Matrix toLocal = mTempMatrix; 420 toLocal.reset(); 421 endParentMatrix.invert(toLocal); 422 423 Matrix startLocal = (Matrix) startValues.values.get(PROPNAME_MATRIX); 424 if (startLocal == null) { 425 startLocal = new Matrix(); 426 startValues.values.put(PROPNAME_MATRIX, startLocal); 427 } 428 429 Matrix startParentMatrix = (Matrix) startValues.values.get(PROPNAME_PARENT_MATRIX); 430 startLocal.postConcat(startParentMatrix); 431 startLocal.postConcat(toLocal); 432 } 433 434 private static void setIdentityTransforms(View view) { 435 setTransforms(view, 0, 0, 0, 1, 1, 0, 0, 0); 436 } 437 438 private static void setTransforms(View view, float translationX, float translationY, 439 float translationZ, float scaleX, float scaleY, float rotationX, 440 float rotationY, float rotationZ) { 441 view.setTranslationX(translationX); 442 view.setTranslationY(translationY); 443 ViewCompat.setTranslationZ(view, translationZ); 444 view.setScaleX(scaleX); 445 view.setScaleY(scaleY); 446 view.setRotationX(rotationX); 447 view.setRotationY(rotationY); 448 view.setRotation(rotationZ); 449 } 450 451 private static class Transforms { 452 453 final float mTranslationX; 454 final float mTranslationY; 455 final float mTranslationZ; 456 final float mScaleX; 457 final float mScaleY; 458 final float mRotationX; 459 final float mRotationY; 460 final float mRotationZ; 461 462 Transforms(View view) { 463 mTranslationX = view.getTranslationX(); 464 mTranslationY = view.getTranslationY(); 465 mTranslationZ = ViewCompat.getTranslationZ(view); 466 mScaleX = view.getScaleX(); 467 mScaleY = view.getScaleY(); 468 mRotationX = view.getRotationX(); 469 mRotationY = view.getRotationY(); 470 mRotationZ = view.getRotation(); 471 } 472 473 public void restore(View view) { 474 setTransforms(view, mTranslationX, mTranslationY, mTranslationZ, mScaleX, mScaleY, 475 mRotationX, mRotationY, mRotationZ); 476 } 477 478 @Override 479 public boolean equals(Object that) { 480 if (!(that instanceof Transforms)) { 481 return false; 482 } 483 Transforms thatTransform = (Transforms) that; 484 return thatTransform.mTranslationX == mTranslationX 485 && thatTransform.mTranslationY == mTranslationY 486 && thatTransform.mTranslationZ == mTranslationZ 487 && thatTransform.mScaleX == mScaleX 488 && thatTransform.mScaleY == mScaleY 489 && thatTransform.mRotationX == mRotationX 490 && thatTransform.mRotationY == mRotationY 491 && thatTransform.mRotationZ == mRotationZ; 492 } 493 494 @Override 495 public int hashCode() { 496 int code = mTranslationX != +0.0f ? Float.floatToIntBits(mTranslationX) : 0; 497 code = 31 * code + (mTranslationY != +0.0f ? Float.floatToIntBits(mTranslationY) : 0); 498 code = 31 * code + (mTranslationZ != +0.0f ? Float.floatToIntBits(mTranslationZ) : 0); 499 code = 31 * code + (mScaleX != +0.0f ? Float.floatToIntBits(mScaleX) : 0); 500 code = 31 * code + (mScaleY != +0.0f ? Float.floatToIntBits(mScaleY) : 0); 501 code = 31 * code + (mRotationX != +0.0f ? Float.floatToIntBits(mRotationX) : 0); 502 code = 31 * code + (mRotationY != +0.0f ? Float.floatToIntBits(mRotationY) : 0); 503 code = 31 * code + (mRotationZ != +0.0f ? Float.floatToIntBits(mRotationZ) : 0); 504 return code; 505 } 506 507 } 508 509 private static class GhostListener extends TransitionListenerAdapter { 510 511 private View mView; 512 private GhostViewImpl mGhostView; 513 514 GhostListener(View view, GhostViewImpl ghostView) { 515 mView = view; 516 mGhostView = ghostView; 517 } 518 519 @Override 520 public void onTransitionEnd(@NonNull Transition transition) { 521 transition.removeListener(this); 522 GhostViewUtils.removeGhost(mView); 523 mView.setTag(R.id.transition_transform, null); 524 mView.setTag(R.id.parent_matrix, null); 525 } 526 527 @Override 528 public void onTransitionPause(@NonNull Transition transition) { 529 mGhostView.setVisibility(View.INVISIBLE); 530 } 531 532 @Override 533 public void onTransitionResume(@NonNull Transition transition) { 534 mGhostView.setVisibility(View.VISIBLE); 535 } 536 537 } 538 539 /** 540 * PathAnimatorMatrix allows the translations and the rest of the matrix to be set 541 * separately. This allows the PathMotion to affect the translations while scale 542 * and rotation are evaluated separately. 543 */ 544 private static class PathAnimatorMatrix { 545 546 private final Matrix mMatrix = new Matrix(); 547 private final View mView; 548 private final float[] mValues; 549 private float mTranslationX; 550 private float mTranslationY; 551 552 PathAnimatorMatrix(View view, float[] values) { 553 mView = view; 554 mValues = values.clone(); 555 mTranslationX = mValues[Matrix.MTRANS_X]; 556 mTranslationY = mValues[Matrix.MTRANS_Y]; 557 setAnimationMatrix(); 558 } 559 560 void setValues(float[] values) { 561 System.arraycopy(values, 0, mValues, 0, values.length); 562 setAnimationMatrix(); 563 } 564 565 void setTranslation(PointF translation) { 566 mTranslationX = translation.x; 567 mTranslationY = translation.y; 568 setAnimationMatrix(); 569 } 570 571 private void setAnimationMatrix() { 572 mValues[Matrix.MTRANS_X] = mTranslationX; 573 mValues[Matrix.MTRANS_Y] = mTranslationY; 574 mMatrix.setValues(mValues); 575 ViewUtils.setAnimationMatrix(mView, mMatrix); 576 } 577 578 Matrix getMatrix() { 579 return mMatrix; 580 } 581 } 582 583} 584