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