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