1 /* 2 * Copyright (C) 2013 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 17 package android.transition; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.animation.PropertyValuesHolder; 24 import android.animation.RectEvaluator; 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.compat.annotation.UnsupportedAppUsage; 28 import android.content.Context; 29 import android.content.res.TypedArray; 30 import android.graphics.Bitmap; 31 import android.graphics.Canvas; 32 import android.graphics.Path; 33 import android.graphics.PointF; 34 import android.graphics.Rect; 35 import android.graphics.drawable.BitmapDrawable; 36 import android.graphics.drawable.Drawable; 37 import android.os.Build; 38 import android.util.AttributeSet; 39 import android.util.Property; 40 import android.view.View; 41 import android.view.ViewGroup; 42 43 import com.android.internal.R; 44 45 import java.util.Map; 46 47 /** 48 * This transition captures the layout bounds of target views before and after 49 * the scene change and animates those changes during the transition. 50 * 51 * <p>A ChangeBounds transition can be described in a resource file by using the 52 * tag <code>changeBounds</code>, using its attributes of 53 * {@link android.R.styleable#ChangeBounds} along with the other standard 54 * attributes of {@link android.R.styleable#Transition}.</p> 55 */ 56 public class ChangeBounds extends Transition { 57 58 private static final String PROPNAME_BOUNDS = "android:changeBounds:bounds"; 59 private static final String PROPNAME_CLIP = "android:changeBounds:clip"; 60 private static final String PROPNAME_PARENT = "android:changeBounds:parent"; 61 private static final String PROPNAME_WINDOW_X = "android:changeBounds:windowX"; 62 private static final String PROPNAME_WINDOW_Y = "android:changeBounds:windowY"; 63 private static final String[] sTransitionProperties = { 64 PROPNAME_BOUNDS, 65 PROPNAME_CLIP, 66 PROPNAME_PARENT, 67 PROPNAME_WINDOW_X, 68 PROPNAME_WINDOW_Y 69 }; 70 71 private static final Property<Drawable, PointF> DRAWABLE_ORIGIN_PROPERTY = 72 new Property<Drawable, PointF>(PointF.class, "boundsOrigin") { 73 private Rect mBounds = new Rect(); 74 75 @Override 76 public void set(Drawable object, PointF value) { 77 object.copyBounds(mBounds); 78 mBounds.offsetTo(Math.round(value.x), Math.round(value.y)); 79 object.setBounds(mBounds); 80 } 81 82 @Override 83 public PointF get(Drawable object) { 84 object.copyBounds(mBounds); 85 return new PointF(mBounds.left, mBounds.top); 86 } 87 }; 88 89 private static final Property<ViewBounds, PointF> TOP_LEFT_PROPERTY = 90 new Property<ViewBounds, PointF>(PointF.class, "topLeft") { 91 @Override 92 public void set(ViewBounds viewBounds, PointF topLeft) { 93 viewBounds.setTopLeft(topLeft); 94 } 95 96 @Override 97 public PointF get(ViewBounds viewBounds) { 98 return null; 99 } 100 }; 101 102 private static final Property<ViewBounds, PointF> BOTTOM_RIGHT_PROPERTY = 103 new Property<ViewBounds, PointF>(PointF.class, "bottomRight") { 104 @Override 105 public void set(ViewBounds viewBounds, PointF bottomRight) { 106 viewBounds.setBottomRight(bottomRight); 107 } 108 109 @Override 110 public PointF get(ViewBounds viewBounds) { 111 return null; 112 } 113 }; 114 115 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) 116 private static final Property<View, PointF> BOTTOM_RIGHT_ONLY_PROPERTY = 117 new Property<View, PointF>(PointF.class, "bottomRight") { 118 @Override 119 public void set(View view, PointF bottomRight) { 120 int left = view.getLeft(); 121 int top = view.getTop(); 122 int right = Math.round(bottomRight.x); 123 int bottom = Math.round(bottomRight.y); 124 view.setLeftTopRightBottom(left, top, right, bottom); 125 } 126 127 @Override 128 public PointF get(View view) { 129 return null; 130 } 131 }; 132 133 private static final Property<View, PointF> TOP_LEFT_ONLY_PROPERTY = 134 new Property<View, PointF>(PointF.class, "topLeft") { 135 @Override 136 public void set(View view, PointF topLeft) { 137 int left = Math.round(topLeft.x); 138 int top = Math.round(topLeft.y); 139 int right = view.getRight(); 140 int bottom = view.getBottom(); 141 view.setLeftTopRightBottom(left, top, right, bottom); 142 } 143 144 @Override 145 public PointF get(View view) { 146 return null; 147 } 148 }; 149 150 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) 151 private static final Property<View, PointF> POSITION_PROPERTY = 152 new Property<View, PointF>(PointF.class, "position") { 153 @Override 154 public void set(View view, PointF topLeft) { 155 int left = Math.round(topLeft.x); 156 int top = Math.round(topLeft.y); 157 int right = left + view.getWidth(); 158 int bottom = top + view.getHeight(); 159 view.setLeftTopRightBottom(left, top, right, bottom); 160 } 161 162 @Override 163 public PointF get(View view) { 164 return null; 165 } 166 }; 167 168 int[] tempLocation = new int[2]; 169 boolean mResizeClip = false; 170 boolean mReparent = false; 171 private static final String LOG_TAG = "ChangeBounds"; 172 173 private static RectEvaluator sRectEvaluator = new RectEvaluator(); 174 ChangeBounds()175 public ChangeBounds() {} 176 ChangeBounds(Context context, AttributeSet attrs)177 public ChangeBounds(Context context, AttributeSet attrs) { 178 super(context, attrs); 179 180 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ChangeBounds); 181 boolean resizeClip = a.getBoolean(R.styleable.ChangeBounds_resizeClip, false); 182 a.recycle(); 183 setResizeClip(resizeClip); 184 } 185 186 @Override getTransitionProperties()187 public String[] getTransitionProperties() { 188 return sTransitionProperties; 189 } 190 191 /** 192 * When <code>resizeClip</code> is true, ChangeBounds resizes the view using the clipBounds 193 * instead of changing the dimensions of the view during the animation. When 194 * <code>resizeClip</code> is false, ChangeBounds resizes the View by changing its dimensions. 195 * 196 * <p>When resizeClip is set to true, the clip bounds is modified by ChangeBounds. Therefore, 197 * {@link android.transition.ChangeClipBounds} is not compatible with ChangeBounds 198 * in this mode.</p> 199 * 200 * @param resizeClip Used to indicate whether the view bounds should be modified or the 201 * clip bounds should be modified by ChangeBounds. 202 * @see android.view.View#setClipBounds(android.graphics.Rect) 203 * @attr ref android.R.styleable#ChangeBounds_resizeClip 204 */ setResizeClip(boolean resizeClip)205 public void setResizeClip(boolean resizeClip) { 206 mResizeClip = resizeClip; 207 } 208 209 /** 210 * Returns true when the ChangeBounds will resize by changing the clip bounds during the 211 * view animation or false when bounds are changed. The default value is false. 212 * 213 * @return true when the ChangeBounds will resize by changing the clip bounds during the 214 * view animation or false when bounds are changed. The default value is false. 215 * @attr ref android.R.styleable#ChangeBounds_resizeClip 216 */ getResizeClip()217 public boolean getResizeClip() { 218 return mResizeClip; 219 } 220 221 /** 222 * Setting this flag tells ChangeBounds to track the before/after parent 223 * of every view using this transition. The flag is not enabled by 224 * default because it requires the parent instances to be the same 225 * in the two scenes or else all parents must use ids to allow 226 * the transition to determine which parents are the same. 227 * 228 * @param reparent true if the transition should track the parent 229 * container of target views and animate parent changes. 230 * @deprecated Use {@link android.transition.ChangeTransform} to handle 231 * transitions between different parents. 232 */ 233 @Deprecated setReparent(boolean reparent)234 public void setReparent(boolean reparent) { 235 mReparent = reparent; 236 } 237 captureValues(TransitionValues values)238 private void captureValues(TransitionValues values) { 239 View view = values.view; 240 241 if (view.isLaidOut() || view.getWidth() != 0 || view.getHeight() != 0) { 242 values.values.put(PROPNAME_BOUNDS, new Rect(view.getLeft(), view.getTop(), 243 view.getRight(), view.getBottom())); 244 values.values.put(PROPNAME_PARENT, values.view.getParent()); 245 if (mReparent) { 246 values.view.getLocationInWindow(tempLocation); 247 values.values.put(PROPNAME_WINDOW_X, tempLocation[0]); 248 values.values.put(PROPNAME_WINDOW_Y, tempLocation[1]); 249 } 250 if (mResizeClip) { 251 values.values.put(PROPNAME_CLIP, view.getClipBounds()); 252 } 253 } 254 } 255 256 @Override captureStartValues(TransitionValues transitionValues)257 public void captureStartValues(TransitionValues transitionValues) { 258 captureValues(transitionValues); 259 } 260 261 @Override captureEndValues(TransitionValues transitionValues)262 public void captureEndValues(TransitionValues transitionValues) { 263 captureValues(transitionValues); 264 } 265 parentMatches(View startParent, View endParent)266 private boolean parentMatches(View startParent, View endParent) { 267 boolean parentMatches = true; 268 if (mReparent) { 269 TransitionValues endValues = getMatchedTransitionValues(startParent, true); 270 if (endValues == null) { 271 parentMatches = startParent == endParent; 272 } else { 273 parentMatches = endParent == endValues.view; 274 } 275 } 276 return parentMatches; 277 } 278 279 @Nullable 280 @Override createAnimator(@onNull final ViewGroup sceneRoot, @Nullable TransitionValues startValues, @Nullable TransitionValues endValues)281 public Animator createAnimator(@NonNull final ViewGroup sceneRoot, 282 @Nullable TransitionValues startValues, 283 @Nullable TransitionValues endValues) { 284 if (startValues == null || endValues == null) { 285 return null; 286 } 287 Map<String, Object> startParentVals = startValues.values; 288 Map<String, Object> endParentVals = endValues.values; 289 ViewGroup startParent = (ViewGroup) startParentVals.get(PROPNAME_PARENT); 290 ViewGroup endParent = (ViewGroup) endParentVals.get(PROPNAME_PARENT); 291 if (startParent == null || endParent == null) { 292 return null; 293 } 294 final View view = endValues.view; 295 if (parentMatches(startParent, endParent)) { 296 Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS); 297 Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS); 298 final int startLeft = startBounds.left; 299 final int endLeft = endBounds.left; 300 final int startTop = startBounds.top; 301 final int endTop = endBounds.top; 302 final int startRight = startBounds.right; 303 final int endRight = endBounds.right; 304 final int startBottom = startBounds.bottom; 305 final int endBottom = endBounds.bottom; 306 final int startWidth = startRight - startLeft; 307 final int startHeight = startBottom - startTop; 308 final int endWidth = endRight - endLeft; 309 final int endHeight = endBottom - endTop; 310 Rect startClip = (Rect) startValues.values.get(PROPNAME_CLIP); 311 Rect endClip = (Rect) endValues.values.get(PROPNAME_CLIP); 312 int numChanges = 0; 313 if ((startWidth != 0 && startHeight != 0) || (endWidth != 0 && endHeight != 0)) { 314 if (startLeft != endLeft || startTop != endTop) ++numChanges; 315 if (startRight != endRight || startBottom != endBottom) ++numChanges; 316 } 317 if ((startClip != null && !startClip.equals(endClip)) || 318 (startClip == null && endClip != null)) { 319 ++numChanges; 320 } 321 if (numChanges > 0) { 322 if (view.getParent() instanceof ViewGroup) { 323 final ViewGroup parent = (ViewGroup) view.getParent(); 324 parent.suppressLayout(true); 325 TransitionListener transitionListener = new TransitionListenerAdapter() { 326 boolean mCanceled = false; 327 328 @Override 329 public void onTransitionCancel(Transition transition) { 330 parent.suppressLayout(false); 331 mCanceled = true; 332 } 333 334 @Override 335 public void onTransitionEnd(Transition transition) { 336 if (!mCanceled) { 337 parent.suppressLayout(false); 338 } 339 transition.removeListener(this); 340 } 341 342 @Override 343 public void onTransitionPause(Transition transition) { 344 parent.suppressLayout(false); 345 } 346 347 @Override 348 public void onTransitionResume(Transition transition) { 349 parent.suppressLayout(true); 350 } 351 }; 352 addListener(transitionListener); 353 } 354 Animator anim; 355 if (!mResizeClip) { 356 view.setLeftTopRightBottom(startLeft, startTop, startRight, startBottom); 357 if (numChanges == 2) { 358 if (startWidth == endWidth && startHeight == endHeight) { 359 Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft, 360 endTop); 361 anim = ObjectAnimator.ofObject(view, POSITION_PROPERTY, null, 362 topLeftPath); 363 } else { 364 final ViewBounds viewBounds = new ViewBounds(view); 365 Path topLeftPath = getPathMotion().getPath(startLeft, startTop, 366 endLeft, endTop); 367 ObjectAnimator topLeftAnimator = ObjectAnimator 368 .ofObject(viewBounds, TOP_LEFT_PROPERTY, null, topLeftPath); 369 370 Path bottomRightPath = getPathMotion().getPath(startRight, startBottom, 371 endRight, endBottom); 372 ObjectAnimator bottomRightAnimator = ObjectAnimator.ofObject(viewBounds, 373 BOTTOM_RIGHT_PROPERTY, null, bottomRightPath); 374 AnimatorSet set = new AnimatorSet(); 375 set.playTogether(topLeftAnimator, bottomRightAnimator); 376 anim = set; 377 set.addListener(new AnimatorListenerAdapter() { 378 // We need a strong reference to viewBounds until the 379 // animator ends. 380 private ViewBounds mViewBounds = viewBounds; 381 }); 382 } 383 } else if (startLeft != endLeft || startTop != endTop) { 384 Path topLeftPath = getPathMotion().getPath(startLeft, startTop, 385 endLeft, endTop); 386 anim = ObjectAnimator.ofObject(view, TOP_LEFT_ONLY_PROPERTY, null, 387 topLeftPath); 388 } else { 389 Path bottomRight = getPathMotion().getPath(startRight, startBottom, 390 endRight, endBottom); 391 anim = ObjectAnimator.ofObject(view, BOTTOM_RIGHT_ONLY_PROPERTY, null, 392 bottomRight); 393 } 394 } else { 395 int maxWidth = Math.max(startWidth, endWidth); 396 int maxHeight = Math.max(startHeight, endHeight); 397 398 view.setLeftTopRightBottom(startLeft, startTop, startLeft + maxWidth, 399 startTop + maxHeight); 400 401 ObjectAnimator positionAnimator = null; 402 if (startLeft != endLeft || startTop != endTop) { 403 Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft, 404 endTop); 405 positionAnimator = ObjectAnimator.ofObject(view, POSITION_PROPERTY, null, 406 topLeftPath); 407 } 408 final Rect finalClip = endClip; 409 if (startClip == null) { 410 startClip = new Rect(0, 0, startWidth, startHeight); 411 } 412 if (endClip == null) { 413 endClip = new Rect(0, 0, endWidth, endHeight); 414 } 415 ObjectAnimator clipAnimator = null; 416 if (!startClip.equals(endClip)) { 417 view.setClipBounds(startClip); 418 clipAnimator = ObjectAnimator.ofObject(view, "clipBounds", sRectEvaluator, 419 startClip, endClip); 420 clipAnimator.addListener(new AnimatorListenerAdapter() { 421 private boolean mIsCanceled; 422 423 @Override 424 public void onAnimationCancel(Animator animation) { 425 mIsCanceled = true; 426 } 427 428 @Override 429 public void onAnimationEnd(Animator animation) { 430 if (!mIsCanceled) { 431 view.setClipBounds(finalClip); 432 view.setLeftTopRightBottom(endLeft, endTop, endRight, 433 endBottom); 434 } 435 } 436 }); 437 } 438 anim = TransitionUtils.mergeAnimators(positionAnimator, 439 clipAnimator); 440 } 441 return anim; 442 } 443 } else { 444 sceneRoot.getLocationInWindow(tempLocation); 445 int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X) - tempLocation[0]; 446 int startY = (Integer) startValues.values.get(PROPNAME_WINDOW_Y) - tempLocation[1]; 447 int endX = (Integer) endValues.values.get(PROPNAME_WINDOW_X) - tempLocation[0]; 448 int endY = (Integer) endValues.values.get(PROPNAME_WINDOW_Y) - tempLocation[1]; 449 // TODO: also handle size changes: check bounds and animate size changes 450 if (startX != endX || startY != endY) { 451 final int width = view.getWidth(); 452 final int height = view.getHeight(); 453 Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 454 Canvas canvas = new Canvas(bitmap); 455 view.draw(canvas); 456 final BitmapDrawable drawable = new BitmapDrawable(bitmap); 457 drawable.setBounds(startX, startY, startX + width, startY + height); 458 final float transitionAlpha = view.getTransitionAlpha(); 459 view.setTransitionAlpha(0); 460 sceneRoot.getOverlay().add(drawable); 461 Path topLeftPath = getPathMotion().getPath(startX, startY, endX, endY); 462 PropertyValuesHolder origin = PropertyValuesHolder.ofObject( 463 DRAWABLE_ORIGIN_PROPERTY, null, topLeftPath); 464 ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, origin); 465 anim.addListener(new AnimatorListenerAdapter() { 466 @Override 467 public void onAnimationEnd(Animator animation) { 468 sceneRoot.getOverlay().remove(drawable); 469 view.setTransitionAlpha(transitionAlpha); 470 } 471 }); 472 return anim; 473 } 474 } 475 return null; 476 } 477 478 private static class ViewBounds { 479 private int mLeft; 480 private int mTop; 481 private int mRight; 482 private int mBottom; 483 private View mView; 484 private int mTopLeftCalls; 485 private int mBottomRightCalls; 486 ViewBounds(View view)487 public ViewBounds(View view) { 488 mView = view; 489 } 490 setTopLeft(PointF topLeft)491 public void setTopLeft(PointF topLeft) { 492 mLeft = Math.round(topLeft.x); 493 mTop = Math.round(topLeft.y); 494 mTopLeftCalls++; 495 if (mTopLeftCalls == mBottomRightCalls) { 496 setLeftTopRightBottom(); 497 } 498 } 499 setBottomRight(PointF bottomRight)500 public void setBottomRight(PointF bottomRight) { 501 mRight = Math.round(bottomRight.x); 502 mBottom = Math.round(bottomRight.y); 503 mBottomRightCalls++; 504 if (mTopLeftCalls == mBottomRightCalls) { 505 setLeftTopRightBottom(); 506 } 507 } 508 setLeftTopRightBottom()509 private void setLeftTopRightBottom() { 510 mView.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); 511 mTopLeftCalls = 0; 512 mBottomRightCalls = 0; 513 } 514 } 515 } 516