Alexander Farber
Alexander Farber

Reputation: 22988

Race condition while showing and hiding FAB with AnimationUtils

In an Android app allowing user logins via social networks I show and hide a FAB using the following code:

app screenshot

public abstract class LoginFragment extends Fragment {

    private FloatingActionButton mFab;
    private Animation mShowFab;
    private Animation mHideFab;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mShowFab = AnimationUtils.makeInAnimation(getContext(), false);
        mShowFab.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
                mFab.setVisibility(View.VISIBLE);
            }

            @Override
            public void onAnimationEnd(Animation animation) {
            }

            @Override
            public void onAnimationRepeat(Animation animation) {
            }
        });

        mHideFab = AnimationUtils.makeOutAnimation(getContext(), true);
        mHideFab.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
            }

            @Override
            public void onAnimationEnd(Animation animation) {
                mFab.setVisibility(View.INVISIBLE);
            }

            @Override
            public void onAnimationRepeat(Animation animation) {
            }
        });
    }

    private void showFab(boolean show) {
        boolean visible = mFab.isShown();

        if (show && !visible) {
                mFab.startAnimation(mShowFab);
        } else if (!show && visible) {
                mFab.startAnimation(mHideFab);
        }
    }

This works well, when I call the above showFab method slow enough.

Before starting any animation I check for current FloatingActionButton visibility, so that the animation is played only once - even if I call for example showFab(true) several times in a row.

My problem:

When a LoginFragment is shown in my app, I first send a request to a ServiceIntent to fetch user data from SQLite and call the following method to set my UI to a "waiting" state:

private void setBusy(boolean busy) {
    mProgressBar.setVisibility(busy ? View.VISIBLE : View.INVISIBLE);
    showFab(!busy);
}

Almost immediately a response from SQLite comes back - via a LocalBroadcastManager and I call the above method again: setBusy(false).

And then the error occurs and the FAB is not visible.

If I replace the FAB method by animation-less code everything works fine:

private void showFab(boolean show) {
    mFab.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
}

But with animation - a racing condition seems to occur.

As a workaround I have tried canceling both animations - but this does not help:

private void showFab(boolean show) {
    mShowFab.cancel();
    mShowFab.reset();

    mHideFab.cancel();
    mHideFab.reset();

    boolean visible = mFab.isShown();

    if (show && !visible) {
            mFab.startAnimation(mShowFab);
    } else if (!show && visible) {
            mFab.startAnimation(mHideFab);
    }
}

Please suggest what could be done here.

I have stepped through my app in debugger numerous times already. The setBusy (and showFab) are called only twice when the Fragment is shown, but both calls happen very quickly - and the FAB is not shown -

First run:

1st run

Second run:

2nd run

UPDATE:

Unfortunately, making the method synchronized does not help either - the FAB stays hidden:

private synchronized void showFab(boolean show) {
    mShowFab.cancel();
    mShowFab.reset();

    mHideFab.cancel();
    mHideFab.reset();

    boolean visible = mFab.isShown();

    if (show && !visible) {
        mFab.startAnimation(mShowFab);
    } else if (!show && visible) {
        mFab.startAnimation(mHideFab);
    }
}

Upvotes: 6

Views: 1108

Answers (3)

Alexander Farber
Alexander Farber

Reputation: 22988

Here is my own solution -

First, using synchronized does not help here, because the animations run in the same thread.

My solution has been to introduce a separate boolean variable to hold the intended final FAB state (visible or not visible):

private FloatingActionButton mFab;
private boolean mFabVisible;
private Animation mShowFab;
private Animation mHideFab;

private void showFab(boolean show) {

    if (show && !mFabVisible) {
        mFab.startAnimation(mShowFab);
    } else if (!show && mFabVisible) {
        mFab.startAnimation(mHideFab);
    }

    mFabVisible = show;
}

Also, I think another possibility might have been to call mFab.setVisibility(View.VISIBLE) twice - as in the below code. But I haven't really tested it:

    mShowFab = AnimationUtils.makeInAnimation(getContext(), false);
    mShowFab.setAnimationListener(new Animation.AnimationListener() {
        @Override
        public void onAnimationStart(Animation animation) {
            mFab.setVisibility(View.VISIBLE);
        }

        @Override
        public void onAnimationEnd(Animation animation) {
            mFab.setVisibility(View.VISIBLE);
        }

        @Override
        public void onAnimationRepeat(Animation animation) {
        }
    });

    mHideFab = AnimationUtils.makeOutAnimation(getContext(), true);
    mHideFab.setAnimationListener(new Animation.AnimationListener() {
        @Override
        public void onAnimationStart(Animation animation) {
        }

        @Override
        public void onAnimationEnd(Animation animation) {
            mFab.setVisibility(View.INVISIBLE);
        }

        @Override
        public void onAnimationRepeat(Animation animation) {
        }
    });

Upvotes: 0

davehenry
davehenry

Reputation: 1076

Each of your animations run in separate threads. This leads to a race condition as you said.

Here's whats going on: showFab is fired with true, then false.

  • The mShowFab onAnimationStart method executes and sets the FAB to visible.
  • The mHideFab onAnimationStart method executes and does nothing.
  • The mShowFab onAnimationEnd method executes and does nothing.
  • The mHideFab onAnimationEnd method executes and sets the FAB to invisible.
  • annnd you end up with an invisible FAB that should be visible.

The variables in your second run show that the mShowFab animation will never run.

You should be able to resolve the race condition by listening for the end of the hide animation. Something like this:

private void showFab(boolean show) {
    if (show) {
        // if you have an animation currently running and you want to show the fab 
        if (mFab.getAnimation() != null && !mFab.getAnimation().hasEnded()) {
            // then wait for it to complete and begin the next one
            mFab.getAnimation().setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {

                }

                @Override
                public void onAnimationEnd(Animation animation) {
                    mFab.setVisibility(View.INVISIBLE);
                    mFab.startAnimation(mShowFab);
                }

                @Override
                public void onAnimationRepeat(Animation animation) {

                }
            });
        } else {
            mFab.startAnimation(mShowFab);
        }
    } else {
        mFab.startAnimation(mHideFab);
    }
}

Upvotes: 3

user3032524
user3032524

Reputation: 86

The first thing to point out is that there is nothing like racing condition. The right terminology is just race condition. Keep this in mind for the future ;-)

I suppose you are using a BroadcastReceiver to receive messages and that you have created the object and tell it to run in a separate thread because of the animation. This means that your showFab method can be called twice in one time. To handle this, define the method as synchronized.

Upvotes: 1

Related Questions