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