Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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);
    }
}
like image 698
Alexander Farber Avatar asked Oct 22 '15 11:10

Alexander Farber


1 Answers

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);
    }
}
like image 166
davehenry Avatar answered Oct 30 '22 17:10

davehenry