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.AnimatorSet; 20 import android.content.Context; 21 import android.content.res.TypedArray; 22 import android.graphics.PointF; 23 24 import android.animation.Animator; 25 import android.animation.AnimatorListenerAdapter; 26 import android.animation.ObjectAnimator; 27 import android.animation.PropertyValuesHolder; 28 import android.animation.RectEvaluator; 29 import android.graphics.Bitmap; 30 import android.graphics.Canvas; 31 import android.graphics.Path; 32 import android.graphics.Rect; 33 import android.graphics.drawable.BitmapDrawable; 34 import android.graphics.drawable.Drawable; 35 import android.util.AttributeSet; 36 import android.util.Property; 37 import android.view.View; 38 import android.view.ViewGroup; 39 40 import com.android.internal.R; 41 42 import java.util.Map; 43 44 /** 45 * This transition captures the layout bounds of target views before and after 46 * the scene change and animates those changes during the transition. 47 * 48 * <p>A ChangeBounds transition can be described in a resource file by using the 49 * tag <code>changeBounds</code>, using its attributes of 50 * {@link android.R.styleable#ChangeBounds} along with the other standard 51 * attributes of {@link android.R.styleable#Transition}.</p> 52 */ 53 public class ChangeBounds extends Transition { 54 55 private static final String PROPNAME_BOUNDS = "android:changeBounds:bounds"; 56 private static final String PROPNAME_CLIP = "android:changeBounds:clip"; 57 private static final String PROPNAME_PARENT = "android:changeBounds:parent"; 58 private static final String PROPNAME_WINDOW_X = "android:changeBounds:windowX"; 59 private static final String PROPNAME_WINDOW_Y = "android:changeBounds:windowY"; 60 private static final String[] sTransitionProperties = { 61 PROPNAME_BOUNDS, 62 PROPNAME_CLIP, 63 PROPNAME_PARENT, 64 PROPNAME_WINDOW_X, 65 PROPNAME_WINDOW_Y 66 }; 67 68 private static final Property<Drawable, PointF> DRAWABLE_ORIGIN_PROPERTY = 69 new Property<Drawable, PointF>(PointF.class, "boundsOrigin") { 70 private Rect mBounds = new Rect(); 71 72 @Override 73 public void set(Drawable object, PointF value) { 74 object.copyBounds(mBounds); 75 mBounds.offsetTo(Math.round(value.x), Math.round(value.y)); 76 object.setBounds(mBounds); 77 } 78 79 @Override 80 public PointF get(Drawable object) { 81 object.copyBounds(mBounds); 82 return new PointF(mBounds.left, mBounds.top); 83 } 84 }; 85 86 private static final Property<ViewBounds, PointF> TOP_LEFT_PROPERTY = 87 new Property<ViewBounds, PointF>(PointF.class, "topLeft") { 88 @Override 89 public void set(ViewBounds viewBounds, PointF topLeft) { 90 viewBounds.setTopLeft(topLeft); 91 } 92 93 @Override 94 public PointF get(ViewBounds viewBounds) { 95 return null; 96 } 97 }; 98 99 private static final Property<ViewBounds, PointF> BOTTOM_RIGHT_PROPERTY = 100 new Property<ViewBounds, PointF>(PointF.class, "bottomRight") { 101 @Override 102 public void set(ViewBounds viewBounds, PointF bottomRight) { 103 viewBounds.setBottomRight(bottomRight); 104 } 105 106 @Override 107 public PointF get(ViewBounds viewBounds) { 108 return null; 109 } 110 }; 111 112 private static final Property<View, PointF> BOTTOM_RIGHT_ONLY_PROPERTY = 113 new Property<View, PointF>(PointF.class, "bottomRight") { 114 @Override 115 public void set(View view, PointF bottomRight) { 116 int left = view.getLeft(); 117 int top = view.getTop(); 118 int right = Math.round(bottomRight.x); 119 int bottom = Math.round(bottomRight.y); 120 view.setLeftTopRightBottom(left, top, right, bottom); 121 } 122 123 @Override 124 public PointF get(View view) { 125 return null; 126 } 127 }; 128 129 private static final Property<View, PointF> TOP_LEFT_ONLY_PROPERTY = 130 new Property<View, PointF>(PointF.class, "topLeft") { 131 @Override 132 public void set(View view, PointF topLeft) { 133 int left = Math.round(topLeft.x); 134 int top = Math.round(topLeft.y); 135 int right = view.getRight(); 136 int bottom = view.getBottom(); 137 view.setLeftTopRightBottom(left, top, right, bottom); 138 } 139 140 @Override 141 public PointF get(View view) { 142 return null; 143 } 144 }; 145 146 private static final Property<View, PointF> POSITION_PROPERTY = 147 new Property<View, PointF>(PointF.class, "position") { 148 @Override 149 public void set(View view, PointF topLeft) { 150 int left = Math.round(topLeft.x); 151 int top = Math.round(topLeft.y); 152 int right = left + view.getWidth(); 153 int bottom = top + view.getHeight(); 154 view.setLeftTopRightBottom(left, top, right, bottom); 155 } 156 157 @Override 158 public PointF get(View view) { 159 return null; 160 } 161 }; 162 163 int[] tempLocation = new int[2]; 164 boolean mResizeClip = false; 165 boolean mReparent = false; 166 private static final String LOG_TAG = "ChangeBounds"; 167 168 private static RectEvaluator sRectEvaluator = new RectEvaluator(); 169 ChangeBounds()170 public ChangeBounds() {} 171 ChangeBounds(Context context, AttributeSet attrs)172 public ChangeBounds(Context context, AttributeSet attrs) { 173 super(context, attrs); 174 175 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ChangeBounds); 176 boolean resizeClip = a.getBoolean(R.styleable.ChangeBounds_resizeClip, false); 177 a.recycle(); 178 setResizeClip(resizeClip); 179 } 180 181 @Override getTransitionProperties()182 public String[] getTransitionProperties() { 183 return sTransitionProperties; 184 } 185 186 /** 187 * When <code>resizeClip</code> is true, ChangeBounds resizes the view using the clipBounds 188 * instead of changing the dimensions of the view during the animation. When 189 * <code>resizeClip</code> is false, ChangeBounds resizes the View by changing its dimensions. 190 * 191 * <p>When resizeClip is set to true, the clip bounds is modified by ChangeBounds. Therefore, 192 * {@link android.transition.ChangeClipBounds} is not compatible with ChangeBounds 193 * in this mode.</p> 194 * 195 * @param resizeClip Used to indicate whether the view bounds should be modified or the 196 * clip bounds should be modified by ChangeBounds. 197 * @see android.view.View#setClipBounds(android.graphics.Rect) 198 * @attr ref android.R.styleable#ChangeBounds_resizeClip 199 */ setResizeClip(boolean resizeClip)200 public void setResizeClip(boolean resizeClip) { 201 mResizeClip = resizeClip; 202 } 203 204 /** 205 * Returns true when the ChangeBounds will resize by changing the clip bounds during the 206 * view animation or false when bounds are changed. The default value is false. 207 * 208 * @return 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 * @attr ref android.R.styleable#ChangeBounds_resizeClip 211 */ getResizeClip()212 public boolean getResizeClip() { 213 return mResizeClip; 214 } 215 216 /** 217 * Setting this flag tells ChangeBounds to track the before/after parent 218 * of every view using this transition. The flag is not enabled by 219 * default because it requires the parent instances to be the same 220 * in the two scenes or else all parents must use ids to allow 221 * the transition to determine which parents are the same. 222 * 223 * @param reparent true if the transition should track the parent 224 * container of target views and animate parent changes. 225 * @deprecated Use {@link android.transition.ChangeTransform} to handle 226 * transitions between different parents. 227 */ setReparent(boolean reparent)228 public void setReparent(boolean reparent) { 229 mReparent = reparent; 230 } 231 captureValues(TransitionValues values)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 captureStartValues(TransitionValues transitionValues)251 public void captureStartValues(TransitionValues transitionValues) { 252 captureValues(transitionValues); 253 } 254 255 @Override captureEndValues(TransitionValues transitionValues)256 public void captureEndValues(TransitionValues transitionValues) { 257 captureValues(transitionValues); 258 } 259 parentMatches(View startParent, View endParent)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 createAnimator(final ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues)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 } 419 420 @Override 421 public void onTransitionPause(Transition transition) { 422 parent.suppressLayout(false); 423 } 424 425 @Override 426 public void onTransitionResume(Transition transition) { 427 parent.suppressLayout(true); 428 } 429 }; 430 addListener(transitionListener); 431 } 432 return anim; 433 } 434 } else { 435 sceneRoot.getLocationInWindow(tempLocation); 436 int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X) - tempLocation[0]; 437 int startY = (Integer) startValues.values.get(PROPNAME_WINDOW_Y) - tempLocation[1]; 438 int endX = (Integer) endValues.values.get(PROPNAME_WINDOW_X) - tempLocation[0]; 439 int endY = (Integer) endValues.values.get(PROPNAME_WINDOW_Y) - tempLocation[1]; 440 // TODO: also handle size changes: check bounds and animate size changes 441 if (startX != endX || startY != endY) { 442 final int width = view.getWidth(); 443 final int height = view.getHeight(); 444 Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 445 Canvas canvas = new Canvas(bitmap); 446 view.draw(canvas); 447 final BitmapDrawable drawable = new BitmapDrawable(bitmap); 448 drawable.setBounds(startX, startY, startX + width, startY + height); 449 final float transitionAlpha = view.getTransitionAlpha(); 450 view.setTransitionAlpha(0); 451 sceneRoot.getOverlay().add(drawable); 452 Path topLeftPath = getPathMotion().getPath(startX, startY, endX, endY); 453 PropertyValuesHolder origin = PropertyValuesHolder.ofObject( 454 DRAWABLE_ORIGIN_PROPERTY, null, topLeftPath); 455 ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, origin); 456 anim.addListener(new AnimatorListenerAdapter() { 457 @Override 458 public void onAnimationEnd(Animator animation) { 459 sceneRoot.getOverlay().remove(drawable); 460 view.setTransitionAlpha(transitionAlpha); 461 } 462 }); 463 return anim; 464 } 465 } 466 return null; 467 } 468 469 private static class ViewBounds { 470 private int mLeft; 471 private int mTop; 472 private int mRight; 473 private int mBottom; 474 private boolean mIsTopLeftSet; 475 private boolean mIsBottomRightSet; 476 private View mView; 477 ViewBounds(View view)478 public ViewBounds(View view) { 479 mView = view; 480 } 481 setTopLeft(PointF topLeft)482 public void setTopLeft(PointF topLeft) { 483 mLeft = Math.round(topLeft.x); 484 mTop = Math.round(topLeft.y); 485 mIsTopLeftSet = true; 486 if (mIsBottomRightSet) { 487 setLeftTopRightBottom(); 488 } 489 } 490 setBottomRight(PointF bottomRight)491 public void setBottomRight(PointF bottomRight) { 492 mRight = Math.round(bottomRight.x); 493 mBottom = Math.round(bottomRight.y); 494 mIsBottomRightSet = true; 495 if (mIsTopLeftSet) { 496 setLeftTopRightBottom(); 497 } 498 } 499 setLeftTopRightBottom()500 private void setLeftTopRightBottom() { 501 mView.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); 502 mIsTopLeftSet = false; 503 mIsBottomRightSet = false; 504 } 505 } 506 } 507