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