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