blob: c32140401fb243f8d670b7f547a6b8d45c706f67 [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.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import android.os.SystemClock;
import android.support.test.filters.SmallTest;
import android.testing.AndroidTestingRunner;
import android.view.View;
import android.widget.FrameLayout;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import com.google.android.collect.Sets;
import org.junit.Before;
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
public void testRenderVisibility() throws InterruptedException {
mLayout.setController(mTestableController);
addOneMoreThanRenderLimitBubbles();
// The last child should be GONE, the rest VISIBLE.
for (int i = 0; i < mMaxRenderedBubbles + 1; i++) {
assertEquals(i == mMaxRenderedBubbles ? View.GONE : View.VISIBLE,
mLayout.getChildAt(i).getVisibility());
}
}
@Test
public void testHierarchyChanges() throws InterruptedException {
mLayout.setController(mTestableController);
addOneMoreThanRenderLimitBubbles();
// 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
public void testUpdateValueNotChained() throws InterruptedException {
mLayout.setController(mTestableController);
addOneMoreThanRenderLimitBubbles();
// 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);
mLayout.animateValueForChildAtIndex(
DynamicAnimation.TRANSLATION_X,
0,
100,
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
public void testUpdateValueXChained() throws InterruptedException {
mLayout.setController(mTestableController);
addOneMoreThanRenderLimitBubbles();
testChainedTranslationAnimations();
}
@Test
public void testSetEndListeners() throws InterruptedException {
mLayout.setController(mTestableController);
addOneMoreThanRenderLimitBubbles();
mTestableController.setChainedProperties(Sets.newHashSet());
final CountDownLatch xLatch = new CountDownLatch(1);
OneTimeEndListener xEndListener = Mockito.spy(new OneTimeEndListener() {
@Override
public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,
float velocity) {
super.onAnimationEnd(animation, canceled, value, velocity);
xLatch.countDown();
}
});
final CountDownLatch yLatch = new CountDownLatch(1);
final OneTimeEndListener yEndListener = Mockito.spy(new OneTimeEndListener() {
@Override
public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,
float velocity) {
super.onAnimationEnd(animation, canceled, value, velocity);
yLatch.countDown();
}
});
// Set end listeners for both x and y.
mLayout.setEndListenerForProperty(xEndListener, DynamicAnimation.TRANSLATION_X);
mLayout.setEndListenerForProperty(yEndListener, DynamicAnimation.TRANSLATION_Y);
// Animate x, and wait for it to finish.
mLayout.animateValueForChildAtIndex(
DynamicAnimation.TRANSLATION_X,
0,
100);
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(xEndListener, Mockito.after(1000).times(1))
.onAnimationEnd(
any(),
eq(false),
eq(100f),
anyFloat());
Mockito.verify(yEndListener, Mockito.after(1000).never())
.onAnimationEnd(any(), anyBoolean(), anyFloat(), anyFloat());
}
@Test
public void testRemoveEndListeners() throws InterruptedException {
mLayout.setController(mTestableController);
addOneMoreThanRenderLimitBubbles();
mTestableController.setChainedProperties(Sets.newHashSet());
final CountDownLatch xLatch = new CountDownLatch(1);
OneTimeEndListener xEndListener = Mockito.spy(new OneTimeEndListener() {
@Override
public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,
float velocity) {
super.onAnimationEnd(animation, canceled, value, velocity);
xLatch.countDown();
}
});
// Set the end listener.
mLayout.setEndListenerForProperty(xEndListener, DynamicAnimation.TRANSLATION_X);
// Animate x, and wait for it to finish.
mLayout.animateValueForChildAtIndex(
DynamicAnimation.TRANSLATION_X,
0,
100);
xLatch.await();
InOrder endListenerCalls = inOrder(xEndListener);
endListenerCalls.verify(xEndListener, Mockito.times(1))
.onAnimationEnd(
any(),
eq(false),
eq(100f),
anyFloat());
// Animate X again, remove the end listener.
mLayout.animateValueForChildAtIndex(
DynamicAnimation.TRANSLATION_X,
0,
1000);
mLayout.removeEndListenerForProperty(DynamicAnimation.TRANSLATION_X);
xLatch.await(1, TimeUnit.SECONDS);
// Make sure the end listener was not called.
endListenerCalls.verifyNoMoreInteractions();
}
@Test
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.
addOneMoreThanRenderLimitBubbles();
mLayout.setController(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.setController(secondController);
mLayout.animateValueForChildAtIndex(
DynamicAnimation.SCALE_X,
0,
1.5f);
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.setController(mTestableController);
mLayout.animateValueForChildAtIndex(
DynamicAnimation.TRANSLATION_X,
0,
100f);
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
public void testArePropertiesAnimating() throws InterruptedException {
mLayout.setController(mTestableController);
addOneMoreThanRenderLimitBubbles();
assertFalse(mLayout.arePropertiesAnimating(
DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y));
mLayout.animateValueForChildAtIndex(
DynamicAnimation.TRANSLATION_X,
0,
100);
// 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
public void testCancelAllAnimations() throws InterruptedException {
mLayout.setController(mTestableController);
addOneMoreThanRenderLimitBubbles();
mLayout.animateValueForChildAtIndex(
DynamicAnimation.TRANSLATION_X,
0,
1000);
mLayout.animateValueForChildAtIndex(
DynamicAnimation.TRANSLATION_Y,
0,
1000);
mLayout.cancelAllAnimations();
// Animations should be somewhere before their end point.
assertTrue(mViews.get(0).getTranslationX() < 1000);
assertTrue(mViews.get(0).getTranslationY() < 1000);
}
@Test
public void testSetChildVisibility() throws InterruptedException {
mLayout.setController(mTestableController);
addOneMoreThanRenderLimitBubbles();
// The last view should have been set to GONE by the controller, since we added one more
// than the limit and it got pushed off. None of the first children should have been set
// VISIBLE, since they would have been animated in by onChildAdded.
Mockito.verify(mTestableController).setChildVisibility(
mViews.get(mViews.size() - 1), 5, View.GONE);
Mockito.verify(mTestableController, never()).setChildVisibility(
any(View.class), anyInt(), eq(View.VISIBLE));
// Remove the first view, which should cause the last view to become visible again.
mLayout.removeView(mViews.get(0));
Mockito.verify(mTestableController).setChildVisibility(
mViews.get(mViews.size() - 1), 4, View.VISIBLE);
}
/** Standard test of chained translation animations. */
private void testChainedTranslationAnimations() throws InterruptedException {
assertEquals(0, mLayout.getChildAt(0).getTranslationX(), .1f);
assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f);
mLayout.animateValueForChildAtIndex(
DynamicAnimation.TRANSLATION_X,
0,
100);
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X);
// Since we enabled chaining, animating the first view to 100 should animate the second to
// 115 (since we set the offset to 15) and the third to 130, etc. Despite the sixth bubble
// not being visible, or animated, make sure that it has the appropriate chained
// translation.
for (int i = 0; i < mMaxRenderedBubbles + 1; 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.
mLayout.animateValueForChildAtIndex(
DynamicAnimation.TRANSLATION_Y,
0,
100);
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);
}
/**
* 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
protected void setChildVisibility(View child, int index, int visibility) {
super.setChildVisibility(child, index, visibility);
}
}
}