• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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