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