blob: 498330c83dcb2f182afcaac0fa6f55adbbed0eac [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.bubbles.animation;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.os.SystemClock;
import android.testing.AndroidTestingRunner;
import android.view.View;
import android.widget.FrameLayout;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import androidx.test.filters.SmallTest;
import com.google.android.collect.Sets;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.mockito.Spy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@SmallTest
@RunWith(AndroidTestingRunner.class)
/** Tests the PhysicsAnimationLayout itself, with a basic test animation controller. */
public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
static final float TEST_TRANSLATION_X_OFFSET = 15f;
@Spy
private TestableAnimationController mTestableController = new TestableAnimationController();
@Before
public void setUp() throws Exception {
super.setUp();
// By default, use translation animations, chain the X animations with the default
// offset, and don't actually remove views immediately (since most implementations will wait
// to animate child views out before actually removing them).
mTestableController.setAnimatedProperties(Sets.newHashSet(
DynamicAnimation.TRANSLATION_X,
DynamicAnimation.TRANSLATION_Y));
mTestableController.setChainedProperties(Sets.newHashSet(DynamicAnimation.TRANSLATION_X));
mTestableController.setOffsetForProperty(
DynamicAnimation.TRANSLATION_X, TEST_TRANSLATION_X_OFFSET);
mTestableController.setRemoveImmediately(false);
}
@Test
@Ignore
public void testHierarchyChanges() throws InterruptedException {
mLayout.setActiveController(mTestableController);
addOneMoreThanBubbleLimitBubbles();
// Make sure the controller was notified of all the views we added.
for (View mView : mViews) {
Mockito.verify(mTestableController).onChildAdded(mView, 0);
}
// Remove some views and ensure the controller was notified, with the proper indices.
mTestableController.setRemoveImmediately(true);
mLayout.removeView(mViews.get(1));
mLayout.removeView(mViews.get(2));
Mockito.verify(mTestableController).onChildRemoved(
eq(mViews.get(1)), eq(1), any());
Mockito.verify(mTestableController).onChildRemoved(
eq(mViews.get(2)), eq(1), any());
// Make sure we still get view added notifications after doing some removals.
final View newBubble = new FrameLayout(mContext);
mLayout.addView(newBubble, 0);
Mockito.verify(mTestableController).onChildAdded(newBubble, 0);
}
@Test
@Ignore
public void testUpdateValueNotChained() throws InterruptedException {
mLayout.setActiveController(mTestableController);
addOneMoreThanBubbleLimitBubbles();
// Don't chain any values.
mTestableController.setChainedProperties(Sets.newHashSet());
// Child views should not be translated.
assertEquals(0, mLayout.getChildAt(0).getTranslationX(), .1f);
assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f);
// Animate the first child's translation X.
final CountDownLatch animLatch = new CountDownLatch(1);
mTestableController
.animationForChildAtIndex(0)
.translationX(100)
.start(animLatch::countDown);
animLatch.await(1, TimeUnit.SECONDS);
// Ensure that the first view has been translated, but not the second one.
assertEquals(100, mLayout.getChildAt(0).getTranslationX(), .1f);
assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f);
}
@Test
@Ignore
public void testUpdateValueXChained() throws InterruptedException {
testChainedTranslationAnimations();
}
@Test
@Ignore
public void testSetEndActions() throws InterruptedException {
mLayout.setActiveController(mTestableController);
addOneMoreThanBubbleLimitBubbles();
mTestableController.setChainedProperties(Sets.newHashSet());
final CountDownLatch xLatch = new CountDownLatch(1);
Runnable xEndAction = Mockito.spy(new Runnable() {
@Override
public void run() {
xLatch.countDown();
}
});
final CountDownLatch yLatch = new CountDownLatch(1);
Runnable yEndAction = Mockito.spy(new Runnable() {
@Override
public void run() {
yLatch.countDown();
}
});
// Set end listeners for both x and y.
mTestableController.setEndActionForProperty(xEndAction, DynamicAnimation.TRANSLATION_X);
mTestableController.setEndActionForProperty(yEndAction, DynamicAnimation.TRANSLATION_Y);
// Animate x, and wait for it to finish.
mTestableController.animationForChildAtIndex(0)
.translationX(100)
.start();
xLatch.await();
yLatch.await(1, TimeUnit.SECONDS);
// Make sure the x end listener was called only one time, and the y listener was never
// called since we didn't animate y. Wait 1 second after the original animation end trigger
// to make sure it doesn't get called again.
Mockito.verify(xEndAction, Mockito.after(1000).times(1)).run();
Mockito.verify(yEndAction, Mockito.after(1000).never()).run();
}
@Test
@Ignore
public void testRemoveEndListeners() throws InterruptedException {
mLayout.setActiveController(mTestableController);
addOneMoreThanBubbleLimitBubbles();
mTestableController.setChainedProperties(Sets.newHashSet());
final CountDownLatch xLatch = new CountDownLatch(1);
Runnable xEndListener = Mockito.spy(new Runnable() {
@Override
public void run() {
xLatch.countDown();
}
});
// Set the end listener.
mTestableController.setEndActionForProperty(xEndListener, DynamicAnimation.TRANSLATION_X);
// Animate x, and wait for it to finish.
mTestableController.animationForChildAtIndex(0)
.translationX(100)
.start();
xLatch.await();
InOrder endListenerCalls = inOrder(xEndListener);
endListenerCalls.verify(xEndListener, Mockito.times(1)).run();
// Animate X again, remove the end listener.
mTestableController.animationForChildAtIndex(0)
.translationX(1000)
.start();
mTestableController.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
xLatch.await(1, TimeUnit.SECONDS);
// Make sure the end listener was not called.
endListenerCalls.verifyNoMoreInteractions();
}
@Test
@Ignore
public void testSetController() throws InterruptedException {
// Add the bubbles, then set the controller, to make sure that a controller added to an
// already-initialized view works correctly.
addOneMoreThanBubbleLimitBubbles();
mLayout.setActiveController(mTestableController);
testChainedTranslationAnimations();
TestableAnimationController secondController =
Mockito.spy(new TestableAnimationController());
secondController.setAnimatedProperties(Sets.newHashSet(
DynamicAnimation.SCALE_X, DynamicAnimation.SCALE_Y));
secondController.setChainedProperties(Sets.newHashSet(
DynamicAnimation.SCALE_X));
secondController.setOffsetForProperty(
DynamicAnimation.SCALE_X, 10f);
secondController.setRemoveImmediately(true);
mLayout.setActiveController(secondController);
mTestableController.animationForChildAtIndex(0)
.scaleX(1.5f)
.start();
waitForPropertyAnimations(DynamicAnimation.SCALE_X);
// Make sure we never asked the original controller about any SCALE animations, that would
// mean the controller wasn't switched over properly.
Mockito.verify(mTestableController, Mockito.never())
.getNextAnimationInChain(eq(DynamicAnimation.SCALE_X), anyInt());
Mockito.verify(mTestableController, Mockito.never())
.getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.SCALE_X));
// Make sure we asked the new controller about its animated properties, and configuration
// options.
Mockito.verify(secondController, Mockito.atLeastOnce())
.getAnimatedProperties();
Mockito.verify(secondController, Mockito.atLeastOnce())
.getNextAnimationInChain(eq(DynamicAnimation.SCALE_X), anyInt());
Mockito.verify(secondController, Mockito.atLeastOnce())
.getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.SCALE_X));
mLayout.setActiveController(mTestableController);
mTestableController.animationForChildAtIndex(0)
.translationX(100f)
.start();
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X);
// Make sure we never asked the second controller about the TRANSLATION_X animation.
Mockito.verify(secondController, Mockito.never())
.getNextAnimationInChain(eq(DynamicAnimation.TRANSLATION_X), anyInt());
Mockito.verify(secondController, Mockito.never())
.getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.TRANSLATION_X));
}
@Test
@Ignore
public void testArePropertiesAnimating() throws InterruptedException {
mLayout.setActiveController(mTestableController);
addOneMoreThanBubbleLimitBubbles();
assertFalse(mLayout.arePropertiesAnimating(
DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y));
mTestableController.animationForChildAtIndex(0)
.translationX(100f)
.start();
// Wait for the animations to get underway.
SystemClock.sleep(50);
assertTrue(mLayout.arePropertiesAnimating(DynamicAnimation.TRANSLATION_X));
assertFalse(mLayout.arePropertiesAnimating(DynamicAnimation.TRANSLATION_Y));
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X);
assertFalse(mLayout.arePropertiesAnimating(
DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y));
}
@Test
@Ignore
public void testCancelAllAnimations() throws InterruptedException {
mLayout.setActiveController(mTestableController);
addOneMoreThanBubbleLimitBubbles();
mTestableController.animationForChildAtIndex(0)
.position(1000, 1000)
.start();
mLayout.cancelAllAnimations();
// Animations should be somewhere before their end point.
assertTrue(mViews.get(0).getTranslationX() < 1000);
assertTrue(mViews.get(0).getTranslationY() < 1000);
}
/** Standard test of chained translation animations. */
private void testChainedTranslationAnimations() throws InterruptedException {
mLayout.setActiveController(mTestableController);
addOneMoreThanBubbleLimitBubbles();
assertEquals(0, mLayout.getChildAt(0).getTranslationX(), .1f);
assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f);
mTestableController.animationForChildAtIndex(0)
.translationX(100f)
.start();
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X);
for (int i = 0; i < mLayout.getChildCount(); i++) {
assertEquals(
100 + i * TEST_TRANSLATION_X_OFFSET,
mLayout.getChildAt(i).getTranslationX(), .1f);
}
// Ensure that the Y translations were unaffected.
assertEquals(0, mLayout.getChildAt(0).getTranslationY(), .1f);
assertEquals(0, mLayout.getChildAt(1).getTranslationY(), .1f);
// Animate the first child's Y translation.
mTestableController.animationForChildAtIndex(0)
.translationY(100f)
.start();
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_Y);
// Ensure that only the first view's Y translation chained, since we only chained X
// translations.
assertEquals(100, mLayout.getChildAt(0).getTranslationY(), .1f);
assertEquals(0, mLayout.getChildAt(1).getTranslationY(), .1f);
}
@Test
@Ignore
public void testPhysicsAnimator() throws InterruptedException {
mLayout.setActiveController(mTestableController);
addOneMoreThanBubbleLimitBubbles();
Runnable afterAll = Mockito.mock(Runnable.class);
Runnable after = Mockito.spy(new Runnable() {
int mCallCount = 0;
@Override
public void run() {
// Make sure that if only one of the animations has finished, we didn't already call
// afterAll.
if (mCallCount == 1) {
Mockito.verifyNoMoreInteractions(afterAll);
}
}
});
// Animate from x = 7 to x = 100, and from y = 100 to 7 = 200, calling 'after' after each
// property's animation completes, then call afterAll when they're all complete.
mTestableController.animationForChildAtIndex(0)
.translationX(7, 100, after)
.translationY(100, 200, after)
.start(afterAll);
// We should have immediately set the 'from' values.
assertEquals(7, mViews.get(0).getTranslationX(), .01f);
assertEquals(100, mViews.get(0).getTranslationY(), .01f);
waitForPropertyAnimations(
DynamicAnimation.TRANSLATION_X,
DynamicAnimation.TRANSLATION_Y);
// We should have called the after callback twice, and afterAll once. We verify in the
// mocked callback that afterAll isn't called before both finish.
Mockito.verify(after, times(2)).run();
Mockito.verify(afterAll).run();
// Make sure we actually animated the views.
assertEquals(100, mViews.get(0).getTranslationX(), .01f);
assertEquals(200, mViews.get(0).getTranslationY(), .01f);
}
@Test
@Ignore
public void testAnimationsForChildrenFromIndex() throws InterruptedException {
// Don't chain since we're going to invoke each animation independently.
mTestableController.setChainedProperties(new HashSet<>());
mLayout.setActiveController(mTestableController);
addOneMoreThanBubbleLimitBubbles();
Runnable allEnd = Mockito.mock(Runnable.class);
mTestableController.animationsForChildrenFromIndex(
1, (index, animation) -> animation.translationX((index - 1) * 50))
.startAll(allEnd);
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X);
assertEquals(0, mViews.get(0).getTranslationX(), .1f);
assertEquals(0, mViews.get(1).getTranslationX(), .1f);
assertEquals(50, mViews.get(2).getTranslationX(), .1f);
assertEquals(100, mViews.get(3).getTranslationX(), .1f);
Mockito.verify(allEnd, times(1)).run();
}
@Test
@Ignore
public void testAnimationsForChildrenFromIndex_noChildren() {
mLayout.setActiveController(mTestableController);
final Runnable after = Mockito.mock(Runnable.class);
mTestableController
.animationsForChildrenFromIndex(0, (index, animation) -> { })
.startAll(after);
verify(after, Mockito.times(1)).run();
}
/**
* Animation controller with configuration methods whose return values can be set by individual
* tests.
*/
private class TestableAnimationController
extends PhysicsAnimationLayout.PhysicsAnimationController {
private Set<DynamicAnimation.ViewProperty> mAnimatedProperties = new HashSet<>();
private Set<DynamicAnimation.ViewProperty> mChainedProperties = new HashSet<>();
private HashMap<DynamicAnimation.ViewProperty, Float> mOffsetForProperty = new HashMap<>();
private boolean mRemoveImmediately = false;
void setAnimatedProperties(
Set<DynamicAnimation.ViewProperty> animatedProperties) {
mAnimatedProperties = animatedProperties;
}
void setChainedProperties(
Set<DynamicAnimation.ViewProperty> chainedProperties) {
mChainedProperties = chainedProperties;
}
void setOffsetForProperty(
DynamicAnimation.ViewProperty property, float offset) {
mOffsetForProperty.put(property, offset);
}
public void setRemoveImmediately(boolean removeImmediately) {
mRemoveImmediately = removeImmediately;
}
@Override
Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
return mAnimatedProperties;
}
@Override
int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
return mChainedProperties.contains(property) ? index + 1 : NONE;
}
@Override
float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
return mOffsetForProperty.getOrDefault(property, 0f);
}
@Override
SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
return new SpringForce();
}
@Override
void onChildAdded(View child, int index) {}
@Override
void onChildRemoved(View child, int index, Runnable finishRemoval) {
if (mRemoveImmediately) {
finishRemoval.run();
}
}
@Override
void onChildReordered(View child, int oldIndex, int newIndex) {}
@Override
void onActiveControllerForLayout(PhysicsAnimationLayout layout) {}
}
}