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