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 */ 16package android.transition; 17 18import android.animation.Animator; 19import android.animation.AnimatorListenerAdapter; 20import android.animation.FloatArrayEvaluator; 21import android.animation.ObjectAnimator; 22import android.animation.PropertyValuesHolder; 23import android.content.Context; 24import android.content.res.TypedArray; 25import android.graphics.Matrix; 26import android.graphics.Path; 27import android.graphics.PointF; 28import android.util.AttributeSet; 29import android.util.Property; 30import android.view.GhostView; 31import android.view.View; 32import android.view.ViewGroup; 33 34import 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 */ 44public 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 99 public ChangeTransform() {} 100 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 */ 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 */ 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 */ 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 */ 177 public void setReparent(boolean reparent) { 178 mReparent = reparent; 179 } 180 181 @Override 182 public String[] getTransitionProperties() { 183 return sTransitionProperties; 184 } 185 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 216 public void captureStartValues(TransitionValues transitionValues) { 217 captureValues(transitionValues); 218 } 219 220 @Override 221 public void captureEndValues(TransitionValues transitionValues) { 222 captureValues(transitionValues); 223 } 224 225 @Override 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 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 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 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 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 409 private static void setIdentityTransforms(View view) { 410 setTransforms(view, 0, 0, 0, 1, 1, 0, 0, 0); 411 } 412 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 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 447 public void restore(View view) { 448 setTransforms(view, translationX, translationY, translationZ, scaleX, scaleY, 449 rotationX, rotationY, rotationZ); 450 } 451 452 @Override 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 474 public GhostListener(View view, View startView, GhostView ghostView) { 475 mView = view; 476 mStartView = startView; 477 mGhostView = ghostView; 478 } 479 480 @Override 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 490 public void onTransitionPause(Transition transition) { 491 mGhostView.setVisibility(View.INVISIBLE); 492 } 493 494 @Override 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 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 520 public void setValues(float[] values) { 521 System.arraycopy(values, 0, mValues, 0, values.length); 522 setAnimationMatrix(); 523 } 524 525 public void setTranslation(PointF translation) { 526 mTranslationX = translation.x; 527 mTranslationY = translation.y; 528 setAnimationMatrix(); 529 } 530 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 538 public Matrix getMatrix() { 539 return mMatrix; 540 } 541 } 542} 543