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())));
+ }
+
}