blob: c32140401fb243f8d670b7f547a6b8d45c706f67 [file] [log] [blame]
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001/*
2 * Copyright (C) 2019 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
17package com.android.systemui.bubbles.animation;
18
19import static org.junit.Assert.assertEquals;
20import static org.junit.Assert.assertFalse;
21import static org.junit.Assert.assertTrue;
22import static org.mockito.ArgumentMatchers.any;
23import static org.mockito.ArgumentMatchers.anyBoolean;
24import static org.mockito.ArgumentMatchers.anyFloat;
25import static org.mockito.ArgumentMatchers.anyInt;
26import static org.mockito.ArgumentMatchers.eq;
27import static org.mockito.Mockito.inOrder;
Joshua Tsuji1575e6b2019-01-30 13:43:28 -050028import static org.mockito.Mockito.never;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080029
30import android.os.SystemClock;
31import android.support.test.filters.SmallTest;
32import android.testing.AndroidTestingRunner;
33import android.view.View;
34import android.widget.FrameLayout;
35
36import androidx.dynamicanimation.animation.DynamicAnimation;
37import androidx.dynamicanimation.animation.SpringForce;
38
39import com.google.android.collect.Sets;
40
41import org.junit.Before;
42import org.junit.Test;
43import org.junit.runner.RunWith;
44import org.mockito.InOrder;
45import org.mockito.Mockito;
46import org.mockito.Spy;
47
48import java.util.HashMap;
49import java.util.HashSet;
50import java.util.Set;
51import java.util.concurrent.CountDownLatch;
52import java.util.concurrent.TimeUnit;
53
54@SmallTest
55@RunWith(AndroidTestingRunner.class)
56/** Tests the PhysicsAnimationLayout itself, with a basic test animation controller. */
57public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
58 static final float TEST_TRANSLATION_X_OFFSET = 15f;
59
60 @Spy
61 private TestableAnimationController mTestableController = new TestableAnimationController();
62
63 @Before
64 public void setUp() throws Exception {
65 super.setUp();
66
67 // By default, use translation animations, chain the X animations with the default
68 // offset, and don't actually remove views immediately (since most implementations will wait
69 // to animate child views out before actually removing them).
70 mTestableController.setAnimatedProperties(Sets.newHashSet(
71 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y));
72 mTestableController.setChainedProperties(Sets.newHashSet(DynamicAnimation.TRANSLATION_X));
73 mTestableController.setOffsetForProperty(
74 DynamicAnimation.TRANSLATION_X, TEST_TRANSLATION_X_OFFSET);
75 mTestableController.setRemoveImmediately(false);
76 }
77
78 @Test
Joshua Tsujia08b6d32019-01-29 16:15:52 -050079 public void testRenderVisibility() throws InterruptedException {
Joshua Tsujib1a796b2019-01-16 15:43:12 -080080 mLayout.setController(mTestableController);
81 addOneMoreThanRenderLimitBubbles();
82
83 // The last child should be GONE, the rest VISIBLE.
84 for (int i = 0; i < mMaxRenderedBubbles + 1; i++) {
85 assertEquals(i == mMaxRenderedBubbles ? View.GONE : View.VISIBLE,
86 mLayout.getChildAt(i).getVisibility());
87 }
88 }
89
90 @Test
Joshua Tsujia08b6d32019-01-29 16:15:52 -050091 public void testHierarchyChanges() throws InterruptedException {
Joshua Tsujib1a796b2019-01-16 15:43:12 -080092 mLayout.setController(mTestableController);
93 addOneMoreThanRenderLimitBubbles();
94
95 // Make sure the controller was notified of all the views we added.
96 for (View mView : mViews) {
97 Mockito.verify(mTestableController).onChildAdded(mView, 0);
98 }
99
100 // Remove some views and ensure the controller was notified, with the proper indices.
101 mTestableController.setRemoveImmediately(true);
102 mLayout.removeView(mViews.get(1));
103 mLayout.removeView(mViews.get(2));
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500104 Mockito.verify(mTestableController).onChildRemoved(
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800105 eq(mViews.get(1)), eq(1), any());
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500106 Mockito.verify(mTestableController).onChildRemoved(
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800107 eq(mViews.get(2)), eq(1), any());
108
109 // Make sure we still get view added notifications after doing some removals.
110 final View newBubble = new FrameLayout(mContext);
111 mLayout.addView(newBubble, 0);
112 Mockito.verify(mTestableController).onChildAdded(newBubble, 0);
113 }
114
115 @Test
116 public void testUpdateValueNotChained() throws InterruptedException {
117 mLayout.setController(mTestableController);
118 addOneMoreThanRenderLimitBubbles();
119
120 // Don't chain any values.
121 mTestableController.setChainedProperties(Sets.newHashSet());
122
123 // Child views should not be translated.
124 assertEquals(0, mLayout.getChildAt(0).getTranslationX(), .1f);
125 assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f);
126
127 // Animate the first child's translation X.
128 final CountDownLatch animLatch = new CountDownLatch(1);
129 mLayout.animateValueForChildAtIndex(
130 DynamicAnimation.TRANSLATION_X,
131 0,
132 100,
133 animLatch::countDown);
134 animLatch.await(1, TimeUnit.SECONDS);
135
136 // Ensure that the first view has been translated, but not the second one.
137 assertEquals(100, mLayout.getChildAt(0).getTranslationX(), .1f);
138 assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f);
139 }
140
141 @Test
142 public void testUpdateValueXChained() throws InterruptedException {
143 mLayout.setController(mTestableController);
144 addOneMoreThanRenderLimitBubbles();
145 testChainedTranslationAnimations();
146 }
147
148 @Test
149 public void testSetEndListeners() throws InterruptedException {
150 mLayout.setController(mTestableController);
151 addOneMoreThanRenderLimitBubbles();
152 mTestableController.setChainedProperties(Sets.newHashSet());
153
154 final CountDownLatch xLatch = new CountDownLatch(1);
155 OneTimeEndListener xEndListener = Mockito.spy(new OneTimeEndListener() {
156 @Override
157 public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,
158 float velocity) {
159 super.onAnimationEnd(animation, canceled, value, velocity);
160 xLatch.countDown();
161 }
162 });
163
164 final CountDownLatch yLatch = new CountDownLatch(1);
165 final OneTimeEndListener yEndListener = Mockito.spy(new OneTimeEndListener() {
166 @Override
167 public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,
168 float velocity) {
169 super.onAnimationEnd(animation, canceled, value, velocity);
170 yLatch.countDown();
171 }
172 });
173
174 // Set end listeners for both x and y.
175 mLayout.setEndListenerForProperty(xEndListener, DynamicAnimation.TRANSLATION_X);
176 mLayout.setEndListenerForProperty(yEndListener, DynamicAnimation.TRANSLATION_Y);
177
178 // Animate x, and wait for it to finish.
179 mLayout.animateValueForChildAtIndex(
180 DynamicAnimation.TRANSLATION_X,
181 0,
182 100);
183 xLatch.await();
184 yLatch.await(1, TimeUnit.SECONDS);
185
186 // Make sure the x end listener was called only one time, and the y listener was never
187 // called since we didn't animate y. Wait 1 second after the original animation end trigger
188 // to make sure it doesn't get called again.
189 Mockito.verify(xEndListener, Mockito.after(1000).times(1))
190 .onAnimationEnd(
191 any(),
192 eq(false),
193 eq(100f),
194 anyFloat());
195 Mockito.verify(yEndListener, Mockito.after(1000).never())
196 .onAnimationEnd(any(), anyBoolean(), anyFloat(), anyFloat());
197 }
198
199 @Test
200 public void testRemoveEndListeners() throws InterruptedException {
201 mLayout.setController(mTestableController);
202 addOneMoreThanRenderLimitBubbles();
203 mTestableController.setChainedProperties(Sets.newHashSet());
204
205 final CountDownLatch xLatch = new CountDownLatch(1);
206 OneTimeEndListener xEndListener = Mockito.spy(new OneTimeEndListener() {
207 @Override
208 public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,
209 float velocity) {
210 super.onAnimationEnd(animation, canceled, value, velocity);
211 xLatch.countDown();
212 }
213 });
214
215 // Set the end listener.
216 mLayout.setEndListenerForProperty(xEndListener, DynamicAnimation.TRANSLATION_X);
217
218 // Animate x, and wait for it to finish.
219 mLayout.animateValueForChildAtIndex(
220 DynamicAnimation.TRANSLATION_X,
221 0,
222 100);
223 xLatch.await();
224
225 InOrder endListenerCalls = inOrder(xEndListener);
226 endListenerCalls.verify(xEndListener, Mockito.times(1))
227 .onAnimationEnd(
228 any(),
229 eq(false),
230 eq(100f),
231 anyFloat());
232
233 // Animate X again, remove the end listener.
234 mLayout.animateValueForChildAtIndex(
235 DynamicAnimation.TRANSLATION_X,
236 0,
237 1000);
238 mLayout.removeEndListenerForProperty(DynamicAnimation.TRANSLATION_X);
239 xLatch.await(1, TimeUnit.SECONDS);
240
241 // Make sure the end listener was not called.
242 endListenerCalls.verifyNoMoreInteractions();
243 }
244
245 @Test
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800246 public void testSetController() throws InterruptedException {
247 // Add the bubbles, then set the controller, to make sure that a controller added to an
248 // already-initialized view works correctly.
249 addOneMoreThanRenderLimitBubbles();
250 mLayout.setController(mTestableController);
251 testChainedTranslationAnimations();
252
253 TestableAnimationController secondController =
254 Mockito.spy(new TestableAnimationController());
255 secondController.setAnimatedProperties(Sets.newHashSet(
256 DynamicAnimation.SCALE_X, DynamicAnimation.SCALE_Y));
257 secondController.setChainedProperties(Sets.newHashSet(
258 DynamicAnimation.SCALE_X));
259 secondController.setOffsetForProperty(
260 DynamicAnimation.SCALE_X, 10f);
261 secondController.setRemoveImmediately(true);
262
263 mLayout.setController(secondController);
264 mLayout.animateValueForChildAtIndex(
265 DynamicAnimation.SCALE_X,
266 0,
267 1.5f);
268
269 waitForPropertyAnimations(DynamicAnimation.SCALE_X);
270
271 // Make sure we never asked the original controller about any SCALE animations, that would
272 // mean the controller wasn't switched over properly.
273 Mockito.verify(mTestableController, Mockito.never())
274 .getNextAnimationInChain(eq(DynamicAnimation.SCALE_X), anyInt());
275 Mockito.verify(mTestableController, Mockito.never())
276 .getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.SCALE_X));
277
278 // Make sure we asked the new controller about its animated properties, and configuration
279 // options.
280 Mockito.verify(secondController, Mockito.atLeastOnce())
281 .getAnimatedProperties();
282 Mockito.verify(secondController, Mockito.atLeastOnce())
283 .getNextAnimationInChain(eq(DynamicAnimation.SCALE_X), anyInt());
284 Mockito.verify(secondController, Mockito.atLeastOnce())
285 .getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.SCALE_X));
286
287 mLayout.setController(mTestableController);
288 mLayout.animateValueForChildAtIndex(
289 DynamicAnimation.TRANSLATION_X,
290 0,
291 100f);
292
293 waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X);
294
295 // Make sure we never asked the second controller about the TRANSLATION_X animation.
296 Mockito.verify(secondController, Mockito.never())
297 .getNextAnimationInChain(eq(DynamicAnimation.TRANSLATION_X), anyInt());
298 Mockito.verify(secondController, Mockito.never())
299 .getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.TRANSLATION_X));
300
301 }
302
303 @Test
304 public void testArePropertiesAnimating() throws InterruptedException {
305 mLayout.setController(mTestableController);
306 addOneMoreThanRenderLimitBubbles();
307
308 assertFalse(mLayout.arePropertiesAnimating(
309 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y));
310
311 mLayout.animateValueForChildAtIndex(
312 DynamicAnimation.TRANSLATION_X,
313 0,
314 100);
315
316 // Wait for the animations to get underway.
317 SystemClock.sleep(50);
318
319 assertTrue(mLayout.arePropertiesAnimating(DynamicAnimation.TRANSLATION_X));
320 assertFalse(mLayout.arePropertiesAnimating(DynamicAnimation.TRANSLATION_Y));
321
322 waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X);
323
324 assertFalse(mLayout.arePropertiesAnimating(
325 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y));
326 }
327
328 @Test
329 public void testCancelAllAnimations() throws InterruptedException {
330 mLayout.setController(mTestableController);
331 addOneMoreThanRenderLimitBubbles();
332
333 mLayout.animateValueForChildAtIndex(
334 DynamicAnimation.TRANSLATION_X,
335 0,
336 1000);
337 mLayout.animateValueForChildAtIndex(
338 DynamicAnimation.TRANSLATION_Y,
339 0,
340 1000);
341
342 mLayout.cancelAllAnimations();
343
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800344 // Animations should be somewhere before their end point.
345 assertTrue(mViews.get(0).getTranslationX() < 1000);
346 assertTrue(mViews.get(0).getTranslationY() < 1000);
347 }
348
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500349 @Test
350 public void testSetChildVisibility() throws InterruptedException {
351 mLayout.setController(mTestableController);
352 addOneMoreThanRenderLimitBubbles();
353
354 // The last view should have been set to GONE by the controller, since we added one more
355 // than the limit and it got pushed off. None of the first children should have been set
356 // VISIBLE, since they would have been animated in by onChildAdded.
357 Mockito.verify(mTestableController).setChildVisibility(
358 mViews.get(mViews.size() - 1), 5, View.GONE);
359 Mockito.verify(mTestableController, never()).setChildVisibility(
360 any(View.class), anyInt(), eq(View.VISIBLE));
361
362 // Remove the first view, which should cause the last view to become visible again.
363 mLayout.removeView(mViews.get(0));
364 Mockito.verify(mTestableController).setChildVisibility(
365 mViews.get(mViews.size() - 1), 4, View.VISIBLE);
366 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800367
368 /** Standard test of chained translation animations. */
369 private void testChainedTranslationAnimations() throws InterruptedException {
370 assertEquals(0, mLayout.getChildAt(0).getTranslationX(), .1f);
371 assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f);
372
373 mLayout.animateValueForChildAtIndex(
374 DynamicAnimation.TRANSLATION_X,
375 0,
376 100);
377
378 waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X);
379
380 // Since we enabled chaining, animating the first view to 100 should animate the second to
381 // 115 (since we set the offset to 15) and the third to 130, etc. Despite the sixth bubble
382 // not being visible, or animated, make sure that it has the appropriate chained
383 // translation.
384 for (int i = 0; i < mMaxRenderedBubbles + 1; i++) {
385 assertEquals(
386 100 + i * TEST_TRANSLATION_X_OFFSET,
387 mLayout.getChildAt(i).getTranslationX(), .1f);
388 }
389
390 // Ensure that the Y translations were unaffected.
391 assertEquals(0, mLayout.getChildAt(0).getTranslationY(), .1f);
392 assertEquals(0, mLayout.getChildAt(1).getTranslationY(), .1f);
393
394 // Animate the first child's Y translation.
395 mLayout.animateValueForChildAtIndex(
396 DynamicAnimation.TRANSLATION_Y,
397 0,
398 100);
399
400 waitForPropertyAnimations(DynamicAnimation.TRANSLATION_Y);
401
402 // Ensure that only the first view's Y translation chained, since we only chained X
403 // translations.
404 assertEquals(100, mLayout.getChildAt(0).getTranslationY(), .1f);
405 assertEquals(0, mLayout.getChildAt(1).getTranslationY(), .1f);
406 }
407
408 /**
409 * Animation controller with configuration methods whose return values can be set by individual
410 * tests.
411 */
412 private class TestableAnimationController
413 extends PhysicsAnimationLayout.PhysicsAnimationController {
414 private Set<DynamicAnimation.ViewProperty> mAnimatedProperties = new HashSet<>();
415 private Set<DynamicAnimation.ViewProperty> mChainedProperties = new HashSet<>();
416 private HashMap<DynamicAnimation.ViewProperty, Float> mOffsetForProperty = new HashMap<>();
417 private boolean mRemoveImmediately = false;
418
419 void setAnimatedProperties(
420 Set<DynamicAnimation.ViewProperty> animatedProperties) {
421 mAnimatedProperties = animatedProperties;
422 }
423
424 void setChainedProperties(
425 Set<DynamicAnimation.ViewProperty> chainedProperties) {
426 mChainedProperties = chainedProperties;
427 }
428
429 void setOffsetForProperty(
430 DynamicAnimation.ViewProperty property, float offset) {
431 mOffsetForProperty.put(property, offset);
432 }
433
434 public void setRemoveImmediately(boolean removeImmediately) {
435 mRemoveImmediately = removeImmediately;
436 }
437
438 @Override
439 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
440 return mAnimatedProperties;
441 }
442
443 @Override
444 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
445 return mChainedProperties.contains(property) ? index + 1 : NONE;
446 }
447
448 @Override
449 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
450 return mOffsetForProperty.getOrDefault(property, 0f);
451 }
452
453 @Override
454 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
455 return new SpringForce();
456 }
457
458 @Override
459 void onChildAdded(View child, int index) {}
460
461 @Override
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500462 void onChildRemoved(View child, int index, Runnable finishRemoval) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800463 if (mRemoveImmediately) {
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500464 finishRemoval.run();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800465 }
466 }
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500467
468 @Override
469 protected void setChildVisibility(View child, int index, int visibility) {
470 super.setChildVisibility(child, index, visibility);
471 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800472 }
473}