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