1/* 2 * Copyright (C) 2016 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 17package android.support.transition; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.AnimatorSet; 22import android.animation.ObjectAnimator; 23import android.animation.PropertyValuesHolder; 24import android.content.Context; 25import android.content.res.TypedArray; 26import android.content.res.XmlResourceParser; 27import android.graphics.Bitmap; 28import android.graphics.Canvas; 29import android.graphics.Path; 30import android.graphics.PointF; 31import android.graphics.Rect; 32import android.graphics.drawable.BitmapDrawable; 33import android.graphics.drawable.Drawable; 34import android.support.annotation.NonNull; 35import android.support.annotation.Nullable; 36import android.support.v4.content.res.TypedArrayUtils; 37import android.support.v4.view.ViewCompat; 38import android.util.AttributeSet; 39import android.util.Property; 40import android.view.View; 41import android.view.ViewGroup; 42 43import 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>, along with the other standard attributes of Transition.</p> 51 */ 52public 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 ViewUtils.setLeftTopRightBottom(view, 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 ViewUtils.setLeftTopRightBottom(view, 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 ViewUtils.setLeftTopRightBottom(view, left, top, right, bottom); 154 } 155 156 @Override 157 public PointF get(View view) { 158 return null; 159 } 160 }; 161 162 private int[] mTempLocation = new int[2]; 163 private boolean mResizeClip = false; 164 private boolean mReparent = false; 165 166 private static RectEvaluator sRectEvaluator = new RectEvaluator(); 167 168 public ChangeBounds() { 169 } 170 171 public ChangeBounds(Context context, AttributeSet attrs) { 172 super(context, attrs); 173 174 TypedArray a = context.obtainStyledAttributes(attrs, Styleable.CHANGE_BOUNDS); 175 boolean resizeClip = TypedArrayUtils.getNamedBoolean(a, (XmlResourceParser) attrs, 176 "resizeClip", Styleable.ChangeBounds.RESIZE_CLIP, false); 177 a.recycle(); 178 setResizeClip(resizeClip); 179 } 180 181 @Nullable 182 @Override 183 public String[] getTransitionProperties() { 184 return sTransitionProperties; 185 } 186 187 /** 188 * When <code>resizeClip</code> is true, ChangeBounds resizes the view using the clipBounds 189 * instead of changing the dimensions of the view during the animation. When 190 * <code>resizeClip</code> is false, ChangeBounds resizes the View by changing its dimensions. 191 * 192 * <p>When resizeClip is set to true, the clip bounds is modified by ChangeBounds. Therefore, 193 * {@link android.transition.ChangeClipBounds} is not compatible with ChangeBounds 194 * in this mode.</p> 195 * 196 * @param resizeClip Used to indicate whether the view bounds should be modified or the 197 * clip bounds should be modified by ChangeBounds. 198 * @see android.view.View#setClipBounds(android.graphics.Rect) 199 */ 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 */ 211 public boolean getResizeClip() { 212 return mResizeClip; 213 } 214 215 private void captureValues(TransitionValues values) { 216 View view = values.view; 217 218 if (ViewCompat.isLaidOut(view) || view.getWidth() != 0 || view.getHeight() != 0) { 219 values.values.put(PROPNAME_BOUNDS, new Rect(view.getLeft(), view.getTop(), 220 view.getRight(), view.getBottom())); 221 values.values.put(PROPNAME_PARENT, values.view.getParent()); 222 if (mReparent) { 223 values.view.getLocationInWindow(mTempLocation); 224 values.values.put(PROPNAME_WINDOW_X, mTempLocation[0]); 225 values.values.put(PROPNAME_WINDOW_Y, mTempLocation[1]); 226 } 227 if (mResizeClip) { 228 values.values.put(PROPNAME_CLIP, ViewCompat.getClipBounds(view)); 229 } 230 } 231 } 232 233 @Override 234 public void captureStartValues(@NonNull TransitionValues transitionValues) { 235 captureValues(transitionValues); 236 } 237 238 @Override 239 public void captureEndValues(@NonNull TransitionValues transitionValues) { 240 captureValues(transitionValues); 241 } 242 243 private boolean parentMatches(View startParent, View endParent) { 244 boolean parentMatches = true; 245 if (mReparent) { 246 TransitionValues endValues = getMatchedTransitionValues(startParent, true); 247 if (endValues == null) { 248 parentMatches = startParent == endParent; 249 } else { 250 parentMatches = endParent == endValues.view; 251 } 252 } 253 return parentMatches; 254 } 255 256 @Override 257 @Nullable 258 public Animator createAnimator(@NonNull final ViewGroup sceneRoot, 259 @Nullable TransitionValues startValues, @Nullable TransitionValues endValues) { 260 if (startValues == null || endValues == null) { 261 return null; 262 } 263 Map<String, Object> startParentVals = startValues.values; 264 Map<String, Object> endParentVals = endValues.values; 265 ViewGroup startParent = (ViewGroup) startParentVals.get(PROPNAME_PARENT); 266 ViewGroup endParent = (ViewGroup) endParentVals.get(PROPNAME_PARENT); 267 if (startParent == null || endParent == null) { 268 return null; 269 } 270 final View view = endValues.view; 271 if (parentMatches(startParent, endParent)) { 272 Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS); 273 Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS); 274 final int startLeft = startBounds.left; 275 final int endLeft = endBounds.left; 276 final int startTop = startBounds.top; 277 final int endTop = endBounds.top; 278 final int startRight = startBounds.right; 279 final int endRight = endBounds.right; 280 final int startBottom = startBounds.bottom; 281 final int endBottom = endBounds.bottom; 282 final int startWidth = startRight - startLeft; 283 final int startHeight = startBottom - startTop; 284 final int endWidth = endRight - endLeft; 285 final int endHeight = endBottom - endTop; 286 Rect startClip = (Rect) startValues.values.get(PROPNAME_CLIP); 287 Rect endClip = (Rect) endValues.values.get(PROPNAME_CLIP); 288 int numChanges = 0; 289 if ((startWidth != 0 && startHeight != 0) || (endWidth != 0 && endHeight != 0)) { 290 if (startLeft != endLeft || startTop != endTop) ++numChanges; 291 if (startRight != endRight || startBottom != endBottom) ++numChanges; 292 } 293 if ((startClip != null && !startClip.equals(endClip)) 294 || (startClip == null && endClip != null)) { 295 ++numChanges; 296 } 297 if (numChanges > 0) { 298 Animator anim; 299 if (!mResizeClip) { 300 ViewUtils.setLeftTopRightBottom(view, startLeft, startTop, startRight, 301 startBottom); 302 if (numChanges == 2) { 303 if (startWidth == endWidth && startHeight == endHeight) { 304 Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft, 305 endTop); 306 anim = ObjectAnimatorUtils.ofPointF(view, POSITION_PROPERTY, 307 topLeftPath); 308 } else { 309 final ViewBounds viewBounds = new ViewBounds(view); 310 Path topLeftPath = getPathMotion().getPath(startLeft, startTop, 311 endLeft, endTop); 312 ObjectAnimator topLeftAnimator = ObjectAnimatorUtils 313 .ofPointF(viewBounds, TOP_LEFT_PROPERTY, topLeftPath); 314 315 Path bottomRightPath = getPathMotion().getPath(startRight, startBottom, 316 endRight, endBottom); 317 ObjectAnimator bottomRightAnimator = ObjectAnimatorUtils.ofPointF( 318 viewBounds, BOTTOM_RIGHT_PROPERTY, bottomRightPath); 319 AnimatorSet set = new AnimatorSet(); 320 set.playTogether(topLeftAnimator, bottomRightAnimator); 321 anim = set; 322 set.addListener(new AnimatorListenerAdapter() { 323 // We need a strong reference to viewBounds until the 324 // animator ends (The ObjectAnimator holds only a weak reference). 325 @SuppressWarnings("unused") 326 private ViewBounds mViewBounds = viewBounds; 327 }); 328 } 329 } else if (startLeft != endLeft || startTop != endTop) { 330 Path topLeftPath = getPathMotion().getPath(startLeft, startTop, 331 endLeft, endTop); 332 anim = ObjectAnimatorUtils.ofPointF(view, TOP_LEFT_ONLY_PROPERTY, 333 topLeftPath); 334 } else { 335 Path bottomRight = getPathMotion().getPath(startRight, startBottom, 336 endRight, endBottom); 337 anim = ObjectAnimatorUtils.ofPointF(view, BOTTOM_RIGHT_ONLY_PROPERTY, 338 bottomRight); 339 } 340 } else { 341 int maxWidth = Math.max(startWidth, endWidth); 342 int maxHeight = Math.max(startHeight, endHeight); 343 344 ViewUtils.setLeftTopRightBottom(view, startLeft, startTop, startLeft + maxWidth, 345 startTop + maxHeight); 346 347 ObjectAnimator positionAnimator = null; 348 if (startLeft != endLeft || startTop != endTop) { 349 Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft, 350 endTop); 351 positionAnimator = ObjectAnimatorUtils.ofPointF(view, POSITION_PROPERTY, 352 topLeftPath); 353 } 354 final Rect finalClip = endClip; 355 if (startClip == null) { 356 startClip = new Rect(0, 0, startWidth, startHeight); 357 } 358 if (endClip == null) { 359 endClip = new Rect(0, 0, endWidth, endHeight); 360 } 361 ObjectAnimator clipAnimator = null; 362 if (!startClip.equals(endClip)) { 363 ViewCompat.setClipBounds(view, startClip); 364 clipAnimator = ObjectAnimator.ofObject(view, "clipBounds", sRectEvaluator, 365 startClip, endClip); 366 clipAnimator.addListener(new AnimatorListenerAdapter() { 367 private boolean mIsCanceled; 368 369 @Override 370 public void onAnimationCancel(Animator animation) { 371 mIsCanceled = true; 372 } 373 374 @Override 375 public void onAnimationEnd(Animator animation) { 376 if (!mIsCanceled) { 377 ViewCompat.setClipBounds(view, finalClip); 378 ViewUtils.setLeftTopRightBottom(view, endLeft, endTop, endRight, 379 endBottom); 380 } 381 } 382 }); 383 } 384 anim = TransitionUtils.mergeAnimators(positionAnimator, 385 clipAnimator); 386 } 387 if (view.getParent() instanceof ViewGroup) { 388 final ViewGroup parent = (ViewGroup) view.getParent(); 389 ViewGroupUtils.suppressLayout(parent, true); 390 TransitionListener transitionListener = new TransitionListenerAdapter() { 391 boolean mCanceled = false; 392 393 @Override 394 public void onTransitionCancel(@NonNull Transition transition) { 395 ViewGroupUtils.suppressLayout(parent, false); 396 mCanceled = true; 397 } 398 399 @Override 400 public void onTransitionEnd(@NonNull Transition transition) { 401 if (!mCanceled) { 402 ViewGroupUtils.suppressLayout(parent, false); 403 } 404 transition.removeListener(this); 405 } 406 407 @Override 408 public void onTransitionPause(@NonNull Transition transition) { 409 ViewGroupUtils.suppressLayout(parent, false); 410 } 411 412 @Override 413 public void onTransitionResume(@NonNull Transition transition) { 414 ViewGroupUtils.suppressLayout(parent, true); 415 } 416 }; 417 addListener(transitionListener); 418 } 419 return anim; 420 } 421 } else { 422 int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X); 423 int startY = (Integer) startValues.values.get(PROPNAME_WINDOW_Y); 424 int endX = (Integer) endValues.values.get(PROPNAME_WINDOW_X); 425 int endY = (Integer) endValues.values.get(PROPNAME_WINDOW_Y); 426 // TODO: also handle size changes: check bounds and animate size changes 427 if (startX != endX || startY != endY) { 428 sceneRoot.getLocationInWindow(mTempLocation); 429 Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), 430 Bitmap.Config.ARGB_8888); 431 Canvas canvas = new Canvas(bitmap); 432 view.draw(canvas); 433 @SuppressWarnings("deprecation") final BitmapDrawable drawable = new BitmapDrawable( 434 bitmap); 435 final float transitionAlpha = ViewUtils.getTransitionAlpha(view); 436 ViewUtils.setTransitionAlpha(view, 0); 437 ViewUtils.getOverlay(sceneRoot).add(drawable); 438 Path topLeftPath = getPathMotion().getPath(startX - mTempLocation[0], 439 startY - mTempLocation[1], endX - mTempLocation[0], 440 endY - mTempLocation[1]); 441 PropertyValuesHolder origin = PropertyValuesHolderUtils.ofPointF( 442 DRAWABLE_ORIGIN_PROPERTY, topLeftPath); 443 ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, origin); 444 anim.addListener(new AnimatorListenerAdapter() { 445 @Override 446 public void onAnimationEnd(Animator animation) { 447 ViewUtils.getOverlay(sceneRoot).remove(drawable); 448 ViewUtils.setTransitionAlpha(view, transitionAlpha); 449 } 450 }); 451 return anim; 452 } 453 } 454 return null; 455 } 456 457 private static class ViewBounds { 458 459 private int mLeft; 460 private int mTop; 461 private int mRight; 462 private int mBottom; 463 private View mView; 464 private int mTopLeftCalls; 465 private int mBottomRightCalls; 466 467 ViewBounds(View view) { 468 mView = view; 469 } 470 471 void setTopLeft(PointF topLeft) { 472 mLeft = Math.round(topLeft.x); 473 mTop = Math.round(topLeft.y); 474 mTopLeftCalls++; 475 if (mTopLeftCalls == mBottomRightCalls) { 476 setLeftTopRightBottom(); 477 } 478 } 479 480 void setBottomRight(PointF bottomRight) { 481 mRight = Math.round(bottomRight.x); 482 mBottom = Math.round(bottomRight.y); 483 mBottomRightCalls++; 484 if (mTopLeftCalls == mBottomRightCalls) { 485 setLeftTopRightBottom(); 486 } 487 } 488 489 private void setLeftTopRightBottom() { 490 ViewUtils.setLeftTopRightBottom(mView, mLeft, mTop, mRight, mBottom); 491 mTopLeftCalls = 0; 492 mBottomRightCalls = 0; 493 } 494 495 } 496 497} 498