Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1575e6bb authored by Joshua Tsuji's avatar Joshua Tsuji
Browse files

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
parent 16f373b0
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -26,7 +26,7 @@ Returns a SpringForce instance to use for animations of the given property. This

### Animation Control Methods
![Diagram of how calls to animateValueForChildAtIndex dispatch to DynamicAnimations.](physics-animation-layout-control-methods.png)
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.

+57 −6
Original line number Diff line number Diff line
@@ -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 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
    void onChildToBeRemoved(View child, int index, Runnable actuallyRemove) {
        // TODO: Animate the bubble out, and pull the other bubbles into its position.
        actuallyRemove.run();
    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;
    }
}
+30 −12
Original line number Diff line number Diff line
@@ -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);
                }
            }
        }
    }

+2 −2
Original line number Diff line number Diff line
@@ -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);

+30 −8
Original line number Diff line number Diff line
@@ -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);
            }
        }
    }
}
Loading