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