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 ChangeTransform()98 public ChangeTransform() {} 99 ChangeTransform(Context context, AttributeSet attrs)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 */ getReparentWithOverlay()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 */ setReparentWithOverlay(boolean reparentWithOverlay)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 */ getReparent()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 */ setReparent(boolean reparent)175 public void setReparent(boolean reparent) { 176 mReparent = reparent; 177 } 178 179 @Override getTransitionProperties()180 public String[] getTransitionProperties() { 181 return sTransitionProperties; 182 } 183 captureValues(TransitionValues transitionValues)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 captureStartValues(TransitionValues transitionValues)214 public void captureStartValues(TransitionValues transitionValues) { 215 captureValues(transitionValues); 216 } 217 218 @Override captureEndValues(TransitionValues transitionValues)219 public void captureEndValues(TransitionValues transitionValues) { 220 captureValues(transitionValues); 221 } 222 223 @Override createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues)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 createTransformAnimator(TransitionValues startValues, TransitionValues endValues, final boolean handleParentChange)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 parentsMatch(ViewGroup startParent, ViewGroup endParent)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 createGhostView(final ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues)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 setMatricesForParent(TransitionValues startValues, TransitionValues endValues)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 setIdentityTransforms(View view)407 private static void setIdentityTransforms(View view) { 408 setTransforms(view, 0, 0, 0, 1, 1, 0, 0, 0); 409 } 410 setTransforms(View view, float translationX, float translationY, float translationZ, float scaleX, float scaleY, float rotationX, float rotationY, float rotationZ)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 Transforms(View view)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 restore(View view)445 public void restore(View view) { 446 setTransforms(view, translationX, translationY, translationZ, scaleX, scaleY, 447 rotationX, rotationY, rotationZ); 448 } 449 450 @Override equals(Object that)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 GhostListener(View view, View startView, GhostView ghostView)472 public GhostListener(View view, View startView, GhostView ghostView) { 473 mView = view; 474 mStartView = startView; 475 mGhostView = ghostView; 476 } 477 478 @Override onTransitionEnd(Transition transition)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 onTransitionPause(Transition transition)488 public void onTransitionPause(Transition transition) { 489 mGhostView.setVisibility(View.INVISIBLE); 490 } 491 492 @Override onTransitionResume(Transition transition)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 PathAnimatorMatrix(View view, float[] values)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 setValues(float[] values)518 public void setValues(float[] values) { 519 System.arraycopy(values, 0, mValues, 0, values.length); 520 setAnimationMatrix(); 521 } 522 setTranslation(PointF translation)523 public void setTranslation(PointF translation) { 524 mTranslationX = translation.x; 525 mTranslationY = translation.y; 526 setAnimationMatrix(); 527 } 528 setAnimationMatrix()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 getMatrix()536 public Matrix getMatrix() { 537 return mMatrix; 538 } 539 } 540 } 541