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