Animate addition/removal of views in expanded mode.
This required adding the setChildVisibility method to controllers, to allow them to animate in/out views that pass the max rendered child threshold. This was not previously relevant since in the bubble stack, you can't really see the views when they're set to VISIBLE/GONE. Also, renamed onChildToBeRemoved to onChildRemoved since that's more accurate given the move to transient views. Test: atest SystemUITests Change-Id: I291ff8f6257ba54e0688c1062bbd673e0c7bdb5c
This commit is contained in:
@@ -26,7 +26,7 @@ Returns a SpringForce instance to use for animations of the given property. This
|
||||
|
||||
### Animation Control Methods
|
||||

|
||||
Once the layout has used the controller’s configuration properties to build the animations, the controller can use them to actually run animations. This is done for two reasons - reacting to a view being added or removed, or responding to another class (such as a touch handler or broadcast receiver) requesting an animation. ```onChildAdded``` and ```onChildRemoved``` are called automatically by the layout, giving the controller the opportunity to animate the child in/out. Custom methods are called by anyone with access to the controller instance to do things like expand, collapse, or move the child views.
|
||||
Once the layout has used the controller’s configuration properties to build the animations, the controller can use them to actually run animations. This is done for two reasons - reacting to a view being added or removed, or responding to another class (such as a touch handler or broadcast receiver) requesting an animation. ```onChildAdded```, ```onChildRemoved```, and ```setChildVisibility``` are called automatically by the layout, giving the controller the opportunity to animate the child in/out/visible/gone. Custom methods are called by anyone with access to the controller instance to do things like expand, collapse, or move the child views.
|
||||
|
||||
In either case, the controller has access to the layout’s protected ```animateValueForChildAtIndex(ViewProperty property, int index, float value)``` method. This method is used to actually run an animation.
|
||||
|
||||
|
||||
@@ -37,6 +37,12 @@ import java.util.Set;
|
||||
public class ExpandedAnimationController
|
||||
extends PhysicsAnimationLayout.PhysicsAnimationController {
|
||||
|
||||
/**
|
||||
* How much to translate the bubbles when they're animating in/out. This value is multiplied by
|
||||
* the bubble size.
|
||||
*/
|
||||
private static final int ANIMATE_TRANSLATION_FACTOR = 4;
|
||||
|
||||
/**
|
||||
* The stack position from which the bubbles were expanded. Saved in {@link #expandFromStack}
|
||||
* and used to return to stack form in {@link #collapseBackToStack}.
|
||||
@@ -125,7 +131,10 @@ public class ExpandedAnimationController
|
||||
Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
|
||||
return Sets.newHashSet(
|
||||
DynamicAnimation.TRANSLATION_X,
|
||||
DynamicAnimation.TRANSLATION_Y);
|
||||
DynamicAnimation.TRANSLATION_Y,
|
||||
DynamicAnimation.SCALE_X,
|
||||
DynamicAnimation.SCALE_Y,
|
||||
DynamicAnimation.ALPHA);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -147,13 +156,55 @@ public class ExpandedAnimationController
|
||||
|
||||
@Override
|
||||
void onChildAdded(View child, int index) {
|
||||
// TODO: Animate the new bubble into the row, and push the other bubbles out of the way.
|
||||
child.setTranslationY(getExpandedY());
|
||||
// Pop in from the top.
|
||||
// TODO: Reverse this when bubbles are at the bottom.
|
||||
child.setTranslationX(getXForChildAtIndex(index));
|
||||
child.setTranslationY(getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR);
|
||||
mLayout.animateValueForChild(DynamicAnimation.TRANSLATION_Y, child, getExpandedY());
|
||||
|
||||
// Animate the remaining bubbles to the correct X position.
|
||||
for (int i = index + 1; i < mLayout.getChildCount(); i++) {
|
||||
mLayout.animateValueForChildAtIndex(
|
||||
DynamicAnimation.TRANSLATION_X, i, getXForChildAtIndex(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void onChildToBeRemoved(View child, int index, Runnable actuallyRemove) {
|
||||
// TODO: Animate the bubble out, and pull the other bubbles into its position.
|
||||
actuallyRemove.run();
|
||||
void onChildRemoved(View child, int index, Runnable finishRemoval) {
|
||||
// Bubble pops out to the top.
|
||||
// TODO: Reverse this when bubbles are at the bottom.
|
||||
mLayout.animateValueForChild(
|
||||
DynamicAnimation.ALPHA, child, 0f, finishRemoval);
|
||||
mLayout.animateValueForChild(
|
||||
DynamicAnimation.TRANSLATION_Y,
|
||||
child,
|
||||
getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR);
|
||||
|
||||
// Animate the remaining bubbles to the correct X position.
|
||||
for (int i = index; i < mLayout.getChildCount(); i++) {
|
||||
mLayout.animateValueForChildAtIndex(
|
||||
DynamicAnimation.TRANSLATION_X, i, getXForChildAtIndex(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setChildVisibility(View child, int index, int visibility) {
|
||||
if (visibility == View.VISIBLE) {
|
||||
// Set alpha to 0 but then become visible immediately so the animation is visible.
|
||||
child.setAlpha(0f);
|
||||
child.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// Fade in.
|
||||
mLayout.animateValueForChild(
|
||||
DynamicAnimation.ALPHA,
|
||||
child,
|
||||
/* value */ visibility == View.GONE ? 0f : 1f,
|
||||
() -> super.setChildVisibility(child, index, visibility));
|
||||
}
|
||||
|
||||
/** Returns the appropriate X translation value for a bubble at the given index. */
|
||||
private float getXForChildAtIndex(int index) {
|
||||
return mBubblePaddingPx + (mBubbleSizePx + mBubblePaddingPx) * index;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,14 +92,18 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
abstract void onChildAdded(View child, int index);
|
||||
|
||||
/**
|
||||
* Called when a child is to be removed from the layout. Controllers can use this
|
||||
* opportunity to animate out the new view before calling the provided callback to actually
|
||||
* remove it.
|
||||
* Called with a child view that has been removed from the layout, from the given index. The
|
||||
* passed view has been removed from the layout and added back as a transient view, which
|
||||
* renders normally, but is not part of the normal view hierarchy and will not be considered
|
||||
* by getChildAt() and getChildCount().
|
||||
*
|
||||
* Controllers should be careful to ensure that actuallyRemove is called on all code paths
|
||||
* or child views will never be removed.
|
||||
* The controller can perform animations on the child (either manually, or by using
|
||||
* {@link #animateValueForChild}), and then call finishRemoval when complete.
|
||||
*
|
||||
* finishRemoval must be called by implementations of this method, or transient views will
|
||||
* never be removed.
|
||||
*/
|
||||
abstract void onChildToBeRemoved(View child, int index, Runnable actuallyRemove);
|
||||
abstract void onChildRemoved(View child, int index, Runnable finishRemoval);
|
||||
|
||||
protected PhysicsAnimationLayout mLayout;
|
||||
|
||||
@@ -112,6 +116,15 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
protected PhysicsAnimationLayout getLayout() {
|
||||
return mLayout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the child's visibility when it moves beyond or within the limits set by a call to
|
||||
* {@link PhysicsAnimationLayout#setMaxRenderedChildren}. This can be overridden to animate
|
||||
* this transition.
|
||||
*/
|
||||
protected void setChildVisibility(View child, int index, int visibility) {
|
||||
child.setVisibility(visibility);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,7 +249,7 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
|
||||
// Tell the controller to animate this view out, and call the callback when it's
|
||||
// finished.
|
||||
mController.onChildToBeRemoved(view, index, () -> {
|
||||
mController.onChildRemoved(view, index, () -> {
|
||||
// Done animating, remove the transient view.
|
||||
removeTransientView(view);
|
||||
|
||||
@@ -457,11 +470,16 @@ public class PhysicsAnimationLayout extends FrameLayout {
|
||||
/** Hides children beyond the max rendering count. */
|
||||
private void setChildrenVisibility() {
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
getChildAt(i).setVisibility(
|
||||
// Ignore views that are animating out when calculating whether to hide the
|
||||
// view. That is, if we're supposed to render 5 views, but 4 are animating out
|
||||
// and will soon be removed, render up to 9 views temporarily.
|
||||
i < mMaxRenderedChildren ? View.VISIBLE : View.GONE);
|
||||
final int targetVisibility = i < mMaxRenderedChildren ? View.VISIBLE : View.GONE;
|
||||
final View targetView = getChildAt(i);
|
||||
|
||||
if (targetView.getVisibility() != targetVisibility) {
|
||||
if (mController != null) {
|
||||
mController.setChildVisibility(targetView, i, targetVisibility);
|
||||
} else {
|
||||
targetView.setVisibility(targetVisibility);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -316,10 +316,10 @@ public class StackAnimationController extends
|
||||
}
|
||||
|
||||
@Override
|
||||
void onChildToBeRemoved(View child, int index, Runnable actuallyRemove) {
|
||||
void onChildRemoved(View child, int index, Runnable finishRemoval) {
|
||||
// Animate the child out, actually removing it once its alpha is zero.
|
||||
mLayout.animateValueForChild(
|
||||
DynamicAnimation.ALPHA, child, 0f, actuallyRemove);
|
||||
DynamicAnimation.ALPHA, child, 0f, finishRemoval);
|
||||
mLayout.animateValueForChild(DynamicAnimation.SCALE_X, child, ANIMATE_IN_STARTING_SCALE);
|
||||
mLayout.animateValueForChild(DynamicAnimation.SCALE_Y, child, ANIMATE_IN_STARTING_SCALE);
|
||||
|
||||
|
||||
@@ -55,11 +55,12 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC
|
||||
mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
|
||||
mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding);
|
||||
mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
|
||||
|
||||
mExpansionPoint = new PointF(100, 100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExpansionAndCollapse() throws InterruptedException {
|
||||
mExpansionPoint = new PointF(100, 100);
|
||||
Runnable afterExpand = Mockito.mock(Runnable.class);
|
||||
mExpandedController.expandFromStack(mExpansionPoint, afterExpand);
|
||||
|
||||
@@ -77,27 +78,48 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC
|
||||
Mockito.verify(afterExpand).run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnChildRemoved() throws InterruptedException {
|
||||
Runnable afterExpand = Mockito.mock(Runnable.class);
|
||||
mExpandedController.expandFromStack(mExpansionPoint, afterExpand);
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
testExpanded();
|
||||
|
||||
// Remove some views and see if the remaining child views still pass the expansion test.
|
||||
mLayout.removeView(mViews.get(0));
|
||||
mLayout.removeView(mViews.get(3));
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
testExpanded();
|
||||
}
|
||||
|
||||
/** Check that children are in the correct positions for being stacked. */
|
||||
private void testStackedAtPosition(float x, float y, int offsetMultiplier) {
|
||||
// Make sure the rest of the stack moved again, including the first bubble not moving, and
|
||||
// is stacked to the right now that we're on the right side of the screen.
|
||||
for (int i = 0; i < mLayout.getChildCount(); i++) {
|
||||
assertEquals(x + i * offsetMultiplier * mStackOffset,
|
||||
mViews.get(i).getTranslationX(), 2f);
|
||||
assertEquals(y, mViews.get(i).getTranslationY(), 2f);
|
||||
mLayout.getChildAt(i).getTranslationX(), 2f);
|
||||
assertEquals(y, mLayout.getChildAt(i).getTranslationY(), 2f);
|
||||
|
||||
if (i < mMaxRenderedBubbles) {
|
||||
assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Check that children are in the correct positions for being expanded. */
|
||||
private void testExpanded() {
|
||||
// Make sure the rest of the stack moved again, including the first bubble not moving, and
|
||||
// is stacked to the right now that we're on the right side of the screen.
|
||||
for (int i = 0; i < mLayout.getChildCount(); i++) {
|
||||
// Check all the visible bubbles to see if they're in the right place.
|
||||
for (int i = 0; i < Math.min(mLayout.getChildCount(), mMaxRenderedBubbles); i++) {
|
||||
assertEquals(mBubblePadding + (i * (mBubbleSize + mBubblePadding)),
|
||||
mViews.get(i).getTranslationX(),
|
||||
mLayout.getChildAt(i).getTranslationX(),
|
||||
2f);
|
||||
assertEquals(mBubblePadding + mCutoutInsetSize,
|
||||
mViews.get(i).getTranslationY(), 2f);
|
||||
mLayout.getChildAt(i).getTranslationY(), 2f);
|
||||
|
||||
if (i < mMaxRenderedBubbles) {
|
||||
assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ 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;
|
||||
@@ -100,9 +101,9 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
|
||||
mTestableController.setRemoveImmediately(true);
|
||||
mLayout.removeView(mViews.get(1));
|
||||
mLayout.removeView(mViews.get(2));
|
||||
Mockito.verify(mTestableController).onChildToBeRemoved(
|
||||
Mockito.verify(mTestableController).onChildRemoved(
|
||||
eq(mViews.get(1)), eq(1), any());
|
||||
Mockito.verify(mTestableController).onChildToBeRemoved(
|
||||
Mockito.verify(mTestableController).onChildRemoved(
|
||||
eq(mViews.get(2)), eq(1), any());
|
||||
|
||||
// Make sure we still get view added notifications after doing some removals.
|
||||
@@ -345,6 +346,24 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
|
||||
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 {
|
||||
@@ -440,10 +459,15 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
|
||||
void onChildAdded(View child, int index) {}
|
||||
|
||||
@Override
|
||||
void onChildToBeRemoved(View child, int index, Runnable actuallyRemove) {
|
||||
void onChildRemoved(View child, int index, Runnable finishRemoval) {
|
||||
if (mRemoveImmediately) {
|
||||
actuallyRemove.run();
|
||||
finishRemoval.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setChildVisibility(View child, int index, int visibility) {
|
||||
super.setChildVisibility(child, index, visibility);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user