1 /* 2 * Copyright (C) 2015 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 package android.transition.cts; 17 18 import static com.android.compatibility.common.util.CtsMockitoUtils.within; 19 20 import static org.junit.Assert.assertEquals; 21 import static org.junit.Assert.assertFalse; 22 import static org.junit.Assert.assertNull; 23 import static org.junit.Assert.assertTrue; 24 import static org.junit.Assert.fail; 25 import static org.mockito.ArgumentMatchers.argThat; 26 import static org.mockito.Mockito.mock; 27 28 import android.animation.Animator; 29 import android.content.res.Resources; 30 import android.graphics.Point; 31 import android.graphics.Rect; 32 import android.transition.ChangeBounds; 33 import android.transition.Scene; 34 import android.transition.Transition; 35 import android.transition.TransitionManager; 36 import android.transition.TransitionValues; 37 import android.util.TypedValue; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.view.ViewTreeObserver; 41 import android.view.animation.LinearInterpolator; 42 43 import androidx.test.filters.MediumTest; 44 import androidx.test.runner.AndroidJUnit4; 45 46 import org.junit.Before; 47 import org.junit.Test; 48 import org.junit.runner.RunWith; 49 import org.mockito.Mockito; 50 51 import java.util.ArrayList; 52 import java.util.List; 53 54 @MediumTest 55 @RunWith(AndroidJUnit4.class) 56 public class ChangeBoundsTest extends BaseTransitionTest { 57 private static final int SMALL_SQUARE_SIZE_DP = 30; 58 private static final int LARGE_SQUARE_SIZE_DP = 50; 59 private static final int SMALL_OFFSET_DP = 2; 60 61 ChangeBounds mChangeBounds; 62 ValidateBoundsListener mBoundsChangeListener; 63 64 @Override 65 @Before setup()66 public void setup() { 67 super.setup(); 68 resetChangeBoundsTransition(); 69 mBoundsChangeListener = null; 70 } 71 resetChangeBoundsTransition()72 private void resetChangeBoundsTransition() { 73 mListener = mock(Transition.TransitionListener.class); 74 mChangeBounds = new MyChangeBounds(); 75 mChangeBounds.setDuration(1000); 76 mChangeBounds.addListener(mListener); 77 mChangeBounds.setInterpolator(new LinearInterpolator()); 78 mTransition = mChangeBounds; 79 } 80 81 @Test testBasicChangeBounds()82 public void testBasicChangeBounds() throws Throwable { 83 enterScene(R.layout.scene1); 84 85 validateInScene1(); 86 87 mBoundsChangeListener = new ValidateBoundsListener(true); 88 89 startTransition(R.layout.scene6); 90 // The update listener will validate that it is changing throughout the animation 91 waitForEnd(5000); 92 93 validateInScene6(); 94 } 95 96 @Test testResizeClip()97 public void testResizeClip() throws Throwable { 98 assertEquals(false, mChangeBounds.getResizeClip()); 99 mChangeBounds.setResizeClip(true); 100 assertEquals(true, mChangeBounds.getResizeClip()); 101 enterScene(R.layout.scene1); 102 103 validateInScene1(); 104 105 mBoundsChangeListener = new ValidateBoundsListener(true); 106 107 startTransition(R.layout.scene6); 108 109 // The update listener will validate that it is changing throughout the animation 110 waitForEnd(5000); 111 112 validateInScene6(); 113 } 114 115 @Test testResizeClipSmaller()116 public void testResizeClipSmaller() throws Throwable { 117 mChangeBounds.setResizeClip(true); 118 enterScene(R.layout.scene6); 119 120 validateInScene6(); 121 122 mBoundsChangeListener = new ValidateBoundsListener(false); 123 startTransition(R.layout.scene1); 124 125 // The update listener will validate that it is changing throughout the animation 126 waitForEnd(5000); 127 128 validateInScene1(); 129 } 130 131 @Test testInterruptSameDestination()132 public void testInterruptSameDestination() throws Throwable { 133 enterScene(R.layout.scene1); 134 135 validateInScene1(); 136 137 List<RedAndGreen> points1 = startTransitionAndWatch(R.layout.scene6); 138 139 waitForSizeIsMiddle(points1); 140 resetChangeBoundsTransition(); 141 List<RedAndGreen> points2 = startTransitionAndWatch(R.layout.scene6); 142 143 waitForEnd(5000); 144 145 assertFalse(isRestartingAnimation(points2, R.layout.scene1)); 146 validateInScene6(); 147 } 148 149 @Test testInterruptSameDestinationResizeClip()150 public void testInterruptSameDestinationResizeClip() throws Throwable { 151 mChangeBounds.setResizeClip(true); 152 enterScene(R.layout.scene1); 153 154 validateInScene1(); 155 156 List<RedAndGreen> points1 = startTransitionAndWatch(R.layout.scene6); 157 158 waitForClipIsMiddle(points1); 159 160 resetChangeBoundsTransition(); 161 mChangeBounds.setResizeClip(true); 162 List<RedAndGreen> points2 = startTransitionAndWatch(R.layout.scene6); 163 waitForEnd(5000); 164 165 assertFalse(isRestartingAnimation(points2, R.layout.scene1)); 166 assertFalse(isRestartingClip(points2, R.layout.scene1)); 167 validateInScene6(); 168 } 169 170 @Test testInterruptWithReverse()171 public void testInterruptWithReverse() throws Throwable { 172 enterScene(R.layout.scene1); 173 174 validateInScene1(); 175 176 List<RedAndGreen> points1 = startTransitionAndWatch(R.layout.scene6); 177 178 waitForSizeIsMiddle(points1); 179 // reverse the transition back to scene1 180 resetChangeBoundsTransition(); 181 List<RedAndGreen> points2 = startTransitionAndWatch(R.layout.scene1); 182 waitForEnd(5000); 183 184 assertFalse(isRestartingAnimation(points2, R.layout.scene1)); 185 validateInScene1(); 186 } 187 188 @Test testInterruptWithReverseResizeClip()189 public void testInterruptWithReverseResizeClip() throws Throwable { 190 mChangeBounds.setResizeClip(true); 191 enterScene(R.layout.scene1); 192 193 validateInScene1(); 194 195 List<RedAndGreen> points1 = startTransitionAndWatch(R.layout.scene6); 196 waitForClipIsMiddle(points1); 197 198 // reverse the transition back to scene1 199 resetChangeBoundsTransition(); 200 mChangeBounds.setResizeClip(true); 201 List<RedAndGreen> points2 = startTransitionAndWatch(R.layout.scene1); 202 waitForEnd(5000); 203 204 assertFalse(isRestartingAnimation(points2, R.layout.scene1)); 205 assertFalse(isRestartingAnimation(points2, R.layout.scene6)); 206 assertFalse(isRestartingClip(points2, R.layout.scene1)); 207 assertFalse(isRestartingClip(points2, R.layout.scene6)); 208 validateInScene1(); 209 } 210 startTransitionAndWatch(int layoutId)211 private List<RedAndGreen> startTransitionAndWatch(int layoutId) throws Throwable { 212 final Scene scene = loadScene(layoutId); 213 final List<RedAndGreen> points = Mockito.spy(new ArrayList<>()); 214 mActivityRule.runOnUiThread(() -> { 215 TransitionManager.go(scene, mTransition); 216 mActivity.getWindow().getDecorView().getViewTreeObserver().addOnDrawListener(() -> { 217 points.add(new RedAndGreen(mActivity)); 218 }); 219 }); 220 return points; 221 } 222 waitForSizeIsMiddle(List<RedAndGreen> points)223 private void waitForSizeIsMiddle(List<RedAndGreen> points) throws Throwable { 224 Resources resources = mActivity.getResources(); 225 float middleSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 226 (SMALL_SQUARE_SIZE_DP + LARGE_SQUARE_SIZE_DP) / 2, resources.getDisplayMetrics()); 227 228 Mockito.verify(points, within(3000)).add(argThat(redAndGreen -> 229 redAndGreen.red.position.width() > middleSize 230 && redAndGreen.red.position.height() > middleSize 231 && redAndGreen.green.position.width() > middleSize 232 && redAndGreen.green.position.height() > middleSize 233 )); 234 } 235 waitForClipIsMiddle(List<RedAndGreen> points)236 private void waitForClipIsMiddle(List<RedAndGreen> points) throws Throwable { 237 Resources resources = mActivity.getResources(); 238 float middleSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 239 (SMALL_SQUARE_SIZE_DP + LARGE_SQUARE_SIZE_DP) / 2, resources.getDisplayMetrics()); 240 241 Mockito.verify(points, within(3000)).add(argThat(redAndGreen -> 242 redAndGreen.red.clip != null 243 && redAndGreen.green.clip != null 244 && redAndGreen.red.clip.width() > middleSize 245 && redAndGreen.red.clip.height() > middleSize 246 && redAndGreen.green.clip.width() > middleSize 247 && redAndGreen.green.clip.height() > middleSize 248 )); 249 } 250 isRestartingAnimation(List<RedAndGreen> points, int startLayoutId)251 private boolean isRestartingAnimation(List<RedAndGreen> points, int startLayoutId) { 252 Resources resources = mActivity.getResources(); 253 float errorPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 254 SMALL_OFFSET_DP, resources.getDisplayMetrics()); 255 256 RedAndGreen start = points.get(0); 257 if (startLayoutId == R.layout.scene1) { 258 float smallSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 259 SMALL_SQUARE_SIZE_DP, resources.getDisplayMetrics()); 260 return start.red.position.top == 0 261 && Math.abs(smallSize - start.green.position.top) < errorPx; 262 } else if (startLayoutId == R.layout.scene6) { 263 float largeSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 264 LARGE_SQUARE_SIZE_DP, resources.getDisplayMetrics()); 265 return start.green.position.top == 0 266 && Math.abs(largeSize - start.red.position.top) < errorPx; 267 } else { 268 fail("Don't know what to do with that layout id"); 269 return false; 270 } 271 } 272 isRestartingClip(List<RedAndGreen> points, int startLayoutId)273 private boolean isRestartingClip(List<RedAndGreen> points, int startLayoutId) { 274 Resources resources = mActivity.getResources(); 275 float errorPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 276 SMALL_OFFSET_DP, resources.getDisplayMetrics()); 277 278 RedAndGreen start = points.get(0); 279 if (startLayoutId == R.layout.scene1) { 280 float smallSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 281 SMALL_SQUARE_SIZE_DP, resources.getDisplayMetrics()); 282 return start.red.clip.width() < smallSize + errorPx 283 && start.green.clip.width() < smallSize + errorPx; 284 } else if (startLayoutId == R.layout.scene6) { 285 float largeSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 286 LARGE_SQUARE_SIZE_DP, resources.getDisplayMetrics()); 287 return start.red.clip.width() > largeSize - errorPx 288 && start.green.clip.width() > largeSize - errorPx; 289 } else { 290 fail("Don't know what to do with that layout id"); 291 return false; 292 } 293 } 294 validateInScene1()295 private void validateInScene1() { 296 validateViewPlacement(R.id.redSquare, R.id.greenSquare, SMALL_SQUARE_SIZE_DP); 297 } 298 validateInScene6()299 private void validateInScene6() { 300 validateViewPlacement(R.id.greenSquare, R.id.redSquare, LARGE_SQUARE_SIZE_DP); 301 } 302 validateViewPlacement(int topViewResource, int bottomViewResource, int dim)303 private void validateViewPlacement(int topViewResource, int bottomViewResource, int dim) { 304 Resources resources = mActivity.getResources(); 305 float expectedDim = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dim, 306 resources.getDisplayMetrics()); 307 View aboveSquare = mActivity.findViewById(topViewResource); 308 assertEquals(0, aboveSquare.getLeft()); 309 assertEquals(0, aboveSquare.getTop()); 310 assertTrue(aboveSquare.getRight() != 0); 311 final int aboveSquareBottom = aboveSquare.getBottom(); 312 assertTrue(aboveSquareBottom != 0); 313 314 View belowSquare = mActivity.findViewById(bottomViewResource); 315 assertEquals(0, belowSquare.getLeft()); 316 assertWithinAPixel(aboveSquareBottom, belowSquare.getTop()); 317 assertWithinAPixel(aboveSquareBottom + aboveSquare.getHeight(), 318 belowSquare.getBottom()); 319 assertWithinAPixel(aboveSquare.getRight(), belowSquare.getRight()); 320 321 assertWithinAPixel(expectedDim, aboveSquare.getHeight()); 322 assertWithinAPixel(expectedDim, aboveSquare.getWidth()); 323 assertWithinAPixel(expectedDim, belowSquare.getHeight()); 324 assertWithinAPixel(expectedDim, belowSquare.getWidth()); 325 326 assertNull(aboveSquare.getClipBounds()); 327 assertNull(belowSquare.getClipBounds()); 328 } 329 isWithinAPixel(float expectedDim, int dim)330 private static boolean isWithinAPixel(float expectedDim, int dim) { 331 return (Math.abs(dim - expectedDim) <= 1); 332 } 333 assertWithinAPixel(float expectedDim, int dim)334 private static void assertWithinAPixel(float expectedDim, int dim) { 335 assertTrue("Expected dimension to be within one pixel of " 336 + expectedDim + ", but was " + dim, isWithinAPixel(expectedDim, dim)); 337 } 338 339 private class MyChangeBounds extends ChangeBounds { 340 private static final String PROPNAME_BOUNDS = "android:changeBounds:bounds"; 341 342 @Override createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues)343 public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, 344 TransitionValues endValues) { 345 Animator animator = super.createAnimator(sceneRoot, startValues, endValues); 346 if (animator != null && mBoundsChangeListener != null) { 347 animator.addListener(mBoundsChangeListener); 348 Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS); 349 Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS); 350 } 351 return animator; 352 } 353 } 354 355 private class ValidateBoundsListener implements ViewTreeObserver.OnDrawListener, 356 Animator.AnimatorListener { 357 final boolean mGrow; 358 final int mMin; 359 final int mMax; 360 361 final Point mRedDimensions = new Point(-1, -1); 362 final Point mGreenDimensions = new Point(-1, -1); 363 364 View mRedSquare; 365 View mGreenSquare; 366 367 boolean mDidChangeSize; 368 ValidateBoundsListener(boolean grow)369 private ValidateBoundsListener(boolean grow) { 370 mGrow = grow; 371 372 Resources resources = mActivity.getResources(); 373 mMin = (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 374 SMALL_SQUARE_SIZE_DP, resources.getDisplayMetrics())); 375 mMax = (int) Math.ceil(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 376 LARGE_SQUARE_SIZE_DP, resources.getDisplayMetrics())); 377 } 378 validateView(View view, Point dimensions)379 public void validateView(View view, Point dimensions) { 380 final String name = view.getTransitionName(); 381 final boolean clipped = mChangeBounds.getResizeClip(); 382 assertEquals(clipped, view.getClipBounds() != null); 383 384 final int width; 385 final int height; 386 if (clipped) { 387 width = view.getClipBounds().width(); 388 height = view.getClipBounds().height(); 389 } else { 390 width = view.getWidth(); 391 height = view.getHeight(); 392 } 393 int newWidth = validateDim(name, "width", dimensions.x, width); 394 int newHeight = validateDim(name, "height", dimensions.y, height); 395 dimensions.set(newWidth, newHeight); 396 } 397 validateDim(String name, String dimen, int lastDim, int newDim)398 private int validateDim(String name, String dimen, int lastDim, int newDim) { 399 int dim = newDim; 400 if (lastDim != -1) { 401 // We must give a pixel's buffer because the top-left and 402 // bottom-right may move independently, causing a rounding error 403 // in size change. 404 if (mGrow) { 405 assertTrue(name + " new " + dimen + " " + newDim 406 + " is less than previous " + lastDim, 407 newDim >= lastDim - 1); 408 dim = Math.max(lastDim, newDim); 409 } else { 410 assertTrue(name + " new " + dimen + " " + newDim 411 + " is more than previous " + lastDim, 412 newDim <= lastDim + 1); 413 dim = Math.min(lastDim, newDim); 414 } 415 if (newDim != lastDim) { 416 mDidChangeSize = true; 417 } 418 } 419 assertTrue(name + " " + dimen + " " + newDim + " must be <= " + mMax, 420 newDim <= mMax); 421 assertTrue(name + " " + dimen + " " + newDim + " must be >= " + mMin, 422 newDim >= mMin); 423 return dim; 424 } 425 426 @Override onDraw()427 public void onDraw() { 428 if (mRedSquare == null) { 429 mRedSquare = mActivity.findViewById(R.id.redSquare); 430 mGreenSquare = mActivity.findViewById(R.id.greenSquare); 431 } 432 validateView(mRedSquare, mRedDimensions); 433 validateView(mGreenSquare, mGreenDimensions); 434 } 435 436 @Override onAnimationStart(Animator animation)437 public void onAnimationStart(Animator animation) { 438 mActivity.getWindow().getDecorView().getViewTreeObserver().addOnDrawListener(this); 439 } 440 441 @Override onAnimationEnd(Animator animation)442 public void onAnimationEnd(Animator animation) { 443 mActivity.getWindow().getDecorView().getViewTreeObserver().removeOnDrawListener(this); 444 assertTrue(mDidChangeSize); 445 } 446 447 @Override onAnimationCancel(Animator animation)448 public void onAnimationCancel(Animator animation) { 449 } 450 451 @Override onAnimationRepeat(Animator animation)452 public void onAnimationRepeat(Animator animation) { 453 } 454 } 455 456 static class RedAndGreen { 457 public final PositionAndClip red; 458 public final PositionAndClip green; 459 RedAndGreen(TransitionActivity activity)460 RedAndGreen(TransitionActivity activity) { 461 View redView = activity.findViewById(R.id.redSquare); 462 red = new PositionAndClip(redView); 463 View greenView = activity.findViewById(R.id.redSquare); 464 green = new PositionAndClip(greenView); 465 } 466 } 467 468 static class PositionAndClip { 469 public final Rect position; 470 public final Rect clip; 471 PositionAndClip(View view)472 PositionAndClip(View view) { 473 this.clip = view.getClipBounds(); 474 this.position = 475 new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); 476 } 477 } 478 } 479 480