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 org.junit.Assert.assertEquals; 19 import static org.junit.Assert.assertFalse; 20 import static org.junit.Assert.assertNull; 21 import static org.junit.Assert.assertTrue; 22 import static org.mockito.Mockito.mock; 23 24 import android.animation.Animator; 25 import android.content.res.Resources; 26 import android.graphics.Point; 27 import android.graphics.Rect; 28 import android.support.test.filters.MediumTest; 29 import android.support.test.runner.AndroidJUnit4; 30 import android.transition.ChangeBounds; 31 import android.transition.Transition; 32 import android.transition.TransitionValues; 33 import android.util.TypedValue; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.view.ViewTreeObserver; 37 import android.view.animation.LinearInterpolator; 38 39 import org.junit.Before; 40 import org.junit.Test; 41 import org.junit.runner.RunWith; 42 43 @MediumTest 44 @RunWith(AndroidJUnit4.class) 45 public class ChangeBoundsTest extends BaseTransitionTest { 46 private static final int SMALL_SQUARE_SIZE_DP = 10; 47 private static final int LARGE_SQUARE_SIZE_DP = 30; 48 private static final int SMALL_OFFSET_DP = 2; 49 50 ChangeBounds mChangeBounds; 51 ValidateBoundsListener mBoundsChangeListener; 52 53 @Override 54 @Before setup()55 public void setup() { 56 super.setup(); 57 resetChangeBoundsTransition(); 58 mBoundsChangeListener = null; 59 } 60 resetChangeBoundsTransition()61 private void resetChangeBoundsTransition() { 62 mListener = mock(Transition.TransitionListener.class); 63 mChangeBounds = new MyChangeBounds(); 64 mChangeBounds.setDuration(400); 65 mChangeBounds.addListener(mListener); 66 mChangeBounds.setInterpolator(new LinearInterpolator()); 67 mTransition = mChangeBounds; 68 } 69 70 @Test testBasicChangeBounds()71 public void testBasicChangeBounds() throws Throwable { 72 enterScene(R.layout.scene1); 73 74 validateInScene1(); 75 76 mBoundsChangeListener = new ValidateBoundsListener(true); 77 78 startTransition(R.layout.scene6); 79 // The update listener will validate that it is changing throughout the animation 80 waitForEnd(800); 81 82 validateInScene6(); 83 } 84 85 @Test testResizeClip()86 public void testResizeClip() throws Throwable { 87 assertEquals(false, mChangeBounds.getResizeClip()); 88 mChangeBounds.setResizeClip(true); 89 assertEquals(true, mChangeBounds.getResizeClip()); 90 enterScene(R.layout.scene1); 91 92 validateInScene1(); 93 94 mBoundsChangeListener = new ValidateBoundsListener(true); 95 96 startTransition(R.layout.scene6); 97 98 // The update listener will validate that it is changing throughout the animation 99 waitForEnd(800); 100 101 validateInScene6(); 102 } 103 104 @Test testResizeClipSmaller()105 public void testResizeClipSmaller() throws Throwable { 106 mChangeBounds.setResizeClip(true); 107 enterScene(R.layout.scene6); 108 109 validateInScene6(); 110 111 mBoundsChangeListener = new ValidateBoundsListener(false); 112 startTransition(R.layout.scene1); 113 114 // The update listener will validate that it is changing throughout the animation 115 waitForEnd(800); 116 117 validateInScene1(); 118 } 119 120 @Test testInterruptSameDestination()121 public void testInterruptSameDestination() throws Throwable { 122 enterScene(R.layout.scene1); 123 124 validateInScene1(); 125 126 startTransition(R.layout.scene6); 127 128 // now delay for at least a few frames before interrupting the transition 129 Thread.sleep(150); 130 resetChangeBoundsTransition(); 131 startTransition(R.layout.scene6); 132 133 assertFalse(isRestartingAnimation()); 134 waitForEnd(1000); 135 validateInScene6(); 136 } 137 138 @Test testInterruptSameDestinationResizeClip()139 public void testInterruptSameDestinationResizeClip() throws Throwable { 140 mChangeBounds.setResizeClip(true); 141 enterScene(R.layout.scene1); 142 143 validateInScene1(); 144 145 startTransition(R.layout.scene6); 146 147 // now delay for at least a few frames before interrupting the transition 148 Thread.sleep(150); 149 150 resetChangeBoundsTransition(); 151 mChangeBounds.setResizeClip(true); 152 startTransition(R.layout.scene6); 153 154 assertFalse(isRestartingAnimation()); 155 assertFalse(isRestartingClip()); 156 waitForEnd(1000); 157 validateInScene6(); 158 } 159 160 @Test testInterruptWithReverse()161 public void testInterruptWithReverse() throws Throwable { 162 enterScene(R.layout.scene1); 163 164 validateInScene1(); 165 166 startTransition(R.layout.scene6); 167 168 // now delay for at least a few frames before reversing 169 Thread.sleep(150); 170 // reverse the transition back to scene1 171 resetChangeBoundsTransition(); 172 startTransition(R.layout.scene1); 173 174 assertFalse(isRestartingAnimation()); 175 waitForEnd(1000); 176 validateInScene1(); 177 } 178 179 @Test testInterruptWithReverseResizeClip()180 public void testInterruptWithReverseResizeClip() throws Throwable { 181 mChangeBounds.setResizeClip(true); 182 enterScene(R.layout.scene1); 183 184 validateInScene1(); 185 186 startTransition(R.layout.scene6); 187 188 // now delay for at least a few frames before reversing 189 Thread.sleep(150); 190 191 // reverse the transition back to scene1 192 resetChangeBoundsTransition(); 193 mChangeBounds.setResizeClip(true); 194 startTransition(R.layout.scene1); 195 196 assertFalse(isRestartingAnimation()); 197 assertFalse(isRestartingClip()); 198 waitForEnd(1000); 199 validateInScene1(); 200 } 201 isRestartingAnimation()202 private boolean isRestartingAnimation() { 203 View red = mActivity.findViewById(R.id.redSquare); 204 View green = mActivity.findViewById(R.id.greenSquare); 205 Resources resources = mActivity.getResources(); 206 float closestDistance = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 207 SMALL_OFFSET_DP, resources.getDisplayMetrics()); 208 return red.getTop() < closestDistance || green.getTop() < closestDistance; 209 } 210 isRestartingClip()211 private boolean isRestartingClip() { 212 Resources resources = mActivity.getResources(); 213 float smallDim = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 214 SMALL_SQUARE_SIZE_DP + SMALL_OFFSET_DP, resources.getDisplayMetrics()); 215 float largeDim = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 216 LARGE_SQUARE_SIZE_DP - SMALL_OFFSET_DP, resources.getDisplayMetrics()); 217 218 View red = mActivity.findViewById(R.id.redSquare); 219 Rect redClip = red.getClipBounds(); 220 View green = mActivity.findViewById(R.id.greenSquare); 221 Rect greenClip = green.getClipBounds(); 222 return redClip == null || redClip.width() < smallDim || redClip.width() > largeDim || 223 greenClip == null || greenClip.width() < smallDim || greenClip.width() > largeDim; 224 } 225 validateInScene1()226 private void validateInScene1() { 227 validateViewPlacement(R.id.redSquare, R.id.greenSquare, SMALL_SQUARE_SIZE_DP); 228 } 229 validateInScene6()230 private void validateInScene6() { 231 validateViewPlacement(R.id.greenSquare, R.id.redSquare, LARGE_SQUARE_SIZE_DP); 232 } 233 validateViewPlacement(int topViewResource, int bottomViewResource, int dim)234 private void validateViewPlacement(int topViewResource, int bottomViewResource, int dim) { 235 Resources resources = mActivity.getResources(); 236 float expectedDim = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dim, 237 resources.getDisplayMetrics()); 238 View aboveSquare = mActivity.findViewById(topViewResource); 239 assertEquals(0, aboveSquare.getLeft()); 240 assertEquals(0, aboveSquare.getTop()); 241 assertTrue(aboveSquare.getRight() != 0); 242 final int aboveSquareBottom = aboveSquare.getBottom(); 243 assertTrue(aboveSquareBottom != 0); 244 245 View belowSquare = mActivity.findViewById(bottomViewResource); 246 assertEquals(0, belowSquare.getLeft()); 247 assertWithinAPixel(aboveSquareBottom, belowSquare.getTop()); 248 assertWithinAPixel(aboveSquareBottom + aboveSquare.getHeight(), 249 belowSquare.getBottom()); 250 assertWithinAPixel(aboveSquare.getRight(), belowSquare.getRight()); 251 252 assertWithinAPixel(expectedDim, aboveSquare.getHeight()); 253 assertWithinAPixel(expectedDim, aboveSquare.getWidth()); 254 assertWithinAPixel(expectedDim, belowSquare.getHeight()); 255 assertWithinAPixel(expectedDim, belowSquare.getWidth()); 256 257 assertNull(aboveSquare.getClipBounds()); 258 assertNull(belowSquare.getClipBounds()); 259 } 260 isWithinAPixel(float expectedDim, int dim)261 private static boolean isWithinAPixel(float expectedDim, int dim) { 262 return (Math.abs(dim - expectedDim) <= 1); 263 } 264 assertWithinAPixel(float expectedDim, int dim)265 private static void assertWithinAPixel(float expectedDim, int dim) { 266 assertTrue("Expected dimension to be within one pixel of " 267 + expectedDim + ", but was " + dim, isWithinAPixel(expectedDim, dim)); 268 } 269 assertNotWithinAPixel(float expectedDim, int dim)270 private static void assertNotWithinAPixel(float expectedDim, int dim) { 271 assertTrue("Expected dimension to not be within one pixel of " 272 + expectedDim + ", but was " + dim, !isWithinAPixel(expectedDim, dim)); 273 } 274 275 private class MyChangeBounds extends ChangeBounds { 276 private static final String PROPNAME_BOUNDS = "android:changeBounds:bounds"; 277 @Override createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues)278 public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, 279 TransitionValues endValues) { 280 Animator animator = super.createAnimator(sceneRoot, startValues, endValues); 281 if (animator != null && mBoundsChangeListener != null) { 282 animator.addListener(mBoundsChangeListener); 283 Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS); 284 Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS); 285 } 286 return animator; 287 } 288 } 289 290 private class ValidateBoundsListener implements ViewTreeObserver.OnDrawListener, 291 Animator.AnimatorListener { 292 final boolean mGrow; 293 final int mMin; 294 final int mMax; 295 296 final Point mRedDimensions = new Point(-1, -1); 297 final Point mGreenDimensions = new Point(-1, -1); 298 299 View mRedSquare; 300 View mGreenSquare; 301 302 boolean mDidChangeSize; 303 ValidateBoundsListener(boolean grow)304 private ValidateBoundsListener(boolean grow) { 305 mGrow = grow; 306 307 Resources resources = mActivity.getResources(); 308 mMin = (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 309 SMALL_SQUARE_SIZE_DP, resources.getDisplayMetrics())); 310 mMax = (int) Math.ceil(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 311 LARGE_SQUARE_SIZE_DP, resources.getDisplayMetrics())); 312 } 313 validateView(View view, Point dimensions)314 public void validateView(View view, Point dimensions) { 315 final String name = view.getTransitionName(); 316 final boolean clipped = mChangeBounds.getResizeClip(); 317 assertEquals(clipped, view.getClipBounds() != null); 318 319 final int width; 320 final int height; 321 if (clipped) { 322 width = view.getClipBounds().width(); 323 height = view.getClipBounds().height(); 324 } else { 325 width = view.getWidth(); 326 height = view.getHeight(); 327 } 328 validateDim(name, "width", dimensions.x, width); 329 validateDim(name, "height", dimensions.y, height); 330 dimensions.set(width, height); 331 } 332 validateDim(String name, String dimen, int lastDim, int newDim)333 private void validateDim(String name, String dimen, int lastDim, int newDim) { 334 if (lastDim != -1) { 335 if (mGrow) { 336 assertTrue(name + " new " + dimen + " " + newDim 337 + " is less than previous " + lastDim, 338 newDim >= lastDim); 339 } else { 340 assertTrue(name + " new " + dimen + " " + newDim 341 + " is more than previous " + lastDim, 342 newDim <= lastDim); 343 } 344 if (newDim != lastDim) { 345 mDidChangeSize = true; 346 } 347 } 348 assertTrue(name + " " + dimen + " " + newDim + " must be <= " + mMax, 349 newDim <= mMax); 350 assertTrue(name + " " + dimen + " " + newDim + " must be >= " + mMin, 351 newDim >= mMin); 352 } 353 354 @Override onDraw()355 public void onDraw() { 356 if (mRedSquare == null) { 357 mRedSquare = mActivity.findViewById(R.id.redSquare); 358 mGreenSquare = mActivity.findViewById(R.id.greenSquare); 359 } 360 validateView(mRedSquare, mRedDimensions); 361 validateView(mGreenSquare, mGreenDimensions); 362 } 363 364 @Override onAnimationStart(Animator animation)365 public void onAnimationStart(Animator animation) { 366 mActivity.getWindow().getDecorView().getViewTreeObserver().addOnDrawListener(this); 367 } 368 369 @Override onAnimationEnd(Animator animation)370 public void onAnimationEnd(Animator animation) { 371 mActivity.getWindow().getDecorView().getViewTreeObserver().removeOnDrawListener(this); 372 assertTrue(mDidChangeSize); 373 } 374 375 @Override onAnimationCancel(Animator animation)376 public void onAnimationCancel(Animator animation) { 377 } 378 379 @Override onAnimationRepeat(Animator animation)380 public void onAnimationRepeat(Animator animation) { 381 } 382 } 383 } 384 385