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