Fix FloatingActionButton hide() and show()

They currently don't work if called in rapid
succession.

Also tidied up some of the internal state of
FAB's impl class.

BUG: 30211619
Change-Id: Ib32bcd9fff276819a8790b9f03c985cf48671a8f
diff --git a/design/ics/android/support/design/widget/FloatingActionButtonIcs.java b/design/ics/android/support/design/widget/FloatingActionButtonIcs.java
index 57ad15c..2ead4c4 100644
--- a/design/ics/android/support/design/widget/FloatingActionButtonIcs.java
+++ b/design/ics/android/support/design/widget/FloatingActionButtonIcs.java
@@ -25,7 +25,11 @@
 
 class FloatingActionButtonIcs extends FloatingActionButtonGingerbread {
 
-    private boolean mIsHiding;
+    private static final int ANIM_STATE_NONE = 0;
+    private static final int ANIM_STATE_HIDING = 1;
+    private static final int ANIM_STATE_SHOWING = 2;
+
+    private int mAnimState = ANIM_STATE_NONE;
     private float mRotation;
 
     FloatingActionButtonIcs(VisibilityAwareImageButton view,
@@ -50,22 +54,16 @@
 
     @Override
     void hide(@Nullable final InternalVisibilityChangedListener listener, final boolean fromUser) {
-        if (mIsHiding || mView.getVisibility() != View.VISIBLE) {
-            // A hide animation is in progress, or we're already hidden. Skip the call
-            if (listener != null) {
-                listener.onHidden();
-            }
+        if (isOrWillBeHidden()) {
+            // We either are or will soon be hidden, skip the call
             return;
         }
 
-        if (!ViewCompat.isLaidOut(mView) || mView.isInEditMode()) {
-            // If the view isn't laid out, or we're in the editor, don't run the animation
-            mView.internalSetVisibility(View.GONE, fromUser);
-            if (listener != null) {
-                listener.onHidden();
-            }
-        } else {
-            mView.animate().cancel();
+        mView.animate().cancel();
+
+        if (shouldAnimateVisibilityChange()) {
+            mAnimState = ANIM_STATE_HIDING;
+
             mView.animate()
                     .scaleX(0f)
                     .scaleY(0f)
@@ -77,20 +75,19 @@
 
                         @Override
                         public void onAnimationStart(Animator animation) {
-                            mIsHiding = true;
-                            mCancelled = false;
                             mView.internalSetVisibility(View.VISIBLE, fromUser);
+                            mCancelled = false;
                         }
 
                         @Override
                         public void onAnimationCancel(Animator animation) {
-                            mIsHiding = false;
                             mCancelled = true;
                         }
 
                         @Override
                         public void onAnimationEnd(Animator animation) {
-                            mIsHiding = false;
+                            mAnimState = ANIM_STATE_NONE;
+
                             if (!mCancelled) {
                                 mView.internalSetVisibility(View.GONE, fromUser);
                                 if (listener != null) {
@@ -99,51 +96,89 @@
                             }
                         }
                     });
+        } else {
+            // If the view isn't laid out, or we're in the editor, don't run the animation
+            mView.internalSetVisibility(View.GONE, fromUser);
+            if (listener != null) {
+                listener.onHidden();
+            }
         }
     }
 
     @Override
     void show(@Nullable final InternalVisibilityChangedListener listener, final boolean fromUser) {
-        if (mIsHiding || mView.getVisibility() != View.VISIBLE) {
-            if (ViewCompat.isLaidOut(mView) && !mView.isInEditMode()) {
-                mView.animate().cancel();
-                if (mView.getVisibility() != View.VISIBLE) {
-                    // If the view isn't visible currently, we'll animate it from a single pixel
-                    mView.setAlpha(0f);
-                    mView.setScaleY(0f);
-                    mView.setScaleX(0f);
-                }
-                mView.animate()
-                        .scaleX(1f)
-                        .scaleY(1f)
-                        .alpha(1f)
-                        .setDuration(SHOW_HIDE_ANIM_DURATION)
-                        .setInterpolator(AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR)
-                        .setListener(new AnimatorListenerAdapter() {
-                            @Override
-                            public void onAnimationStart(Animator animation) {
-                                mView.internalSetVisibility(View.VISIBLE, fromUser);
-                            }
+        if (isOrWillBeShown()) {
+            // We either are or will soon be visible, skip the call
+            return;
+        }
 
-                            @Override
-                            public void onAnimationEnd(Animator animation) {
-                                if (listener != null) {
-                                    listener.onShown();
-                                }
+        mView.animate().cancel();
+
+        if (shouldAnimateVisibilityChange()) {
+            mAnimState = ANIM_STATE_SHOWING;
+
+            if (mView.getVisibility() != View.VISIBLE) {
+                // If the view isn't visible currently, we'll animate it from a single pixel
+                mView.setAlpha(0f);
+                mView.setScaleY(0f);
+                mView.setScaleX(0f);
+            }
+
+            mView.animate()
+                    .scaleX(1f)
+                    .scaleY(1f)
+                    .alpha(1f)
+                    .setDuration(SHOW_HIDE_ANIM_DURATION)
+                    .setInterpolator(AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR)
+                    .setListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationStart(Animator animation) {
+                            mView.internalSetVisibility(View.VISIBLE, fromUser);
+                        }
+
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            mAnimState = ANIM_STATE_NONE;
+                            if (listener != null) {
+                                listener.onShown();
                             }
-                        });
-            } else {
-                mView.internalSetVisibility(View.VISIBLE, fromUser);
-                mView.setAlpha(1f);
-                mView.setScaleY(1f);
-                mView.setScaleX(1f);
-                if (listener != null) {
-                    listener.onShown();
-                }
+                        }
+                    });
+        } else {
+            mView.internalSetVisibility(View.VISIBLE, fromUser);
+            mView.setAlpha(1f);
+            mView.setScaleY(1f);
+            mView.setScaleX(1f);
+            if (listener != null) {
+                listener.onShown();
             }
         }
     }
 
+    private boolean isOrWillBeShown() {
+        if (mView.getVisibility() != View.VISIBLE) {
+            // If we not currently visible, return true if we're animating to be shown
+            return mAnimState == ANIM_STATE_SHOWING;
+        } else {
+            // Otherwise if we're visible, return true if we're not animating to be hidden
+            return mAnimState != ANIM_STATE_HIDING;
+        }
+    }
+
+    private boolean isOrWillBeHidden() {
+        if (mView.getVisibility() == View.VISIBLE) {
+            // If we currently visible, return true if we're animating to be hidden
+            return mAnimState == ANIM_STATE_HIDING;
+        } else {
+            // Otherwise if we're not visible, return true if we're not animating to be shown
+            return mAnimState != ANIM_STATE_SHOWING;
+        }
+    }
+
+    private boolean shouldAnimateVisibilityChange() {
+        return ViewCompat.isLaidOut(mView) && !mView.isInEditMode();
+    }
+
     private void updateFromViewRotation() {
         if (Build.VERSION.SDK_INT == 19) {
             // KitKat seems to have an issue with views which are rotated with angles which are
diff --git a/design/tests/src/android/support/design/testutils/FloatingActionButtonActions.java b/design/tests/src/android/support/design/testutils/FloatingActionButtonActions.java
index ad8068b..59b0d48 100644
--- a/design/tests/src/android/support/design/testutils/FloatingActionButtonActions.java
+++ b/design/tests/src/android/support/design/testutils/FloatingActionButtonActions.java
@@ -129,4 +129,54 @@
         };
     }
 
+    public static ViewAction hideThenShow(final int animDuration) {
+        return new ViewAction() {
+            @Override
+            public Matcher<View> getConstraints() {
+                return isAssignableFrom(FloatingActionButton.class);
+            }
+
+            @Override
+            public String getDescription() {
+                return "Calls hide() then show()";
+            }
+
+            @Override
+            public void perform(UiController uiController, View view) {
+                uiController.loopMainThreadUntilIdle();
+
+                FloatingActionButton fab = (FloatingActionButton) view;
+                fab.hide();
+                fab.show();
+
+                uiController.loopMainThreadForAtLeast(animDuration + 100);
+            }
+        };
+    }
+
+    public static ViewAction showThenHide(final int animDuration) {
+        return new ViewAction() {
+            @Override
+            public Matcher<View> getConstraints() {
+                return isAssignableFrom(FloatingActionButton.class);
+            }
+
+            @Override
+            public String getDescription() {
+                return "Calls show() then hide()";
+            }
+
+            @Override
+            public void perform(UiController uiController, View view) {
+                uiController.loopMainThreadUntilIdle();
+
+                FloatingActionButton fab = (FloatingActionButton) view;
+                fab.show();
+                fab.hide();
+
+                uiController.loopMainThreadForAtLeast(animDuration + 50);
+            }
+        };
+    }
+
 }
diff --git a/design/tests/src/android/support/design/widget/FloatingActionButtonTest.java b/design/tests/src/android/support/design/widget/FloatingActionButtonTest.java
index 6927732..417fa35 100644
--- a/design/tests/src/android/support/design/widget/FloatingActionButtonTest.java
+++ b/design/tests/src/android/support/design/widget/FloatingActionButtonTest.java
@@ -16,15 +16,19 @@
 
 package android.support.design.widget;
 
+import static android.support.design.testutils.FloatingActionButtonActions.hideThenShow;
 import static android.support.design.testutils.FloatingActionButtonActions.setBackgroundTintColor;
 import static android.support.design.testutils.FloatingActionButtonActions.setImageResource;
 import static android.support.design.testutils.FloatingActionButtonActions.setLayoutGravity;
 import static android.support.design.testutils.FloatingActionButtonActions.setSize;
+import static android.support.design.testutils.FloatingActionButtonActions.showThenHide;
 import static android.support.design.testutils.TestUtilsMatchers.withFabBackgroundFill;
 import static android.support.design.testutils.TestUtilsMatchers.withFabContentAreaOnMargins;
 import static android.support.design.testutils.TestUtilsMatchers.withFabContentHeight;
+import static android.support.design.widget.DesignViewActions.setVisibility;
 import static android.support.test.espresso.Espresso.onView;
 import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
 import static android.support.test.espresso.matcher.ViewMatchers.withId;
 
 import android.graphics.Color;
@@ -32,6 +36,7 @@
 import android.support.design.testutils.TestUtils;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.view.Gravity;
+import android.view.View;
 
 import org.junit.Test;
 
@@ -114,4 +119,20 @@
                 .check(matches(withFabContentAreaOnMargins(Gravity.RIGHT | Gravity.BOTTOM)));
     }
 
+    @Test
+    public void testHideShow() {
+        onView(withId(R.id.fab_standard))
+                .perform(setVisibility(View.VISIBLE))
+                .perform(hideThenShow(FloatingActionButtonImpl.SHOW_HIDE_ANIM_DURATION))
+                .check(matches(isDisplayed()));
+    }
+
+    @Test
+    public void testShowHide() {
+        onView(withId(R.id.fab_standard))
+                .perform(setVisibility(View.GONE))
+                .perform(showThenHide(FloatingActionButtonImpl.SHOW_HIDE_ANIM_DURATION))
+                .check(matches(not(isDisplayed())));
+    }
+
 }