Race state when showing and hiding FAB with AnimationUtils

In an Android application that allows users on social networks to show and hide the FAB using the following code:

application 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 showFab method above rather slow.

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

My problem:

When LoginFragment displayed in my application, I first send a request to ServiceIntent to retrieve user data from SQLite and the following method is called to set my user interface to standby:

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

Almost immediately, the response from SQLite is returned - through the LocalBroadcastManager , and I again call the method above: setBusy(false) .

And then an error occurs, and the FAB not displayed.

If I replaced the FAB method with code without animation, everything would be fine:

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

But with animation, a racing condition arises.

As a workaround, I tried to cancel 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 can be done here.

I have already gone through my application in the debugger many times. setBusy (and showFab ) is only called twice when a fragment is displayed, but both calls happen very quickly - and FAB not displayed -

First start:

1st run

Second run:

2nd run

UPDATE:

Unfortunately, the synchronized method does not help either - the FAB remains 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); } } 
+5
source share
3 answers

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

Here's what happens: showFab starts with true, then false.

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

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

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

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

The first thing to note is not something like a racing condition. Correct terminology is simply a race condition . Keep this in mind in the future; -)

I assume that you are using BroadcastReceiver to receive messages and that you created the object and tell it to start in a separate thread due to the animation. This means that your showFab method can be called twice at a time. To handle this, define the method as synchronized .

+1
source

Here is my own solution -

Firstly, using synchronized doesn't help here, because the animation runs in the same thread.

My solution was to introduce a separate boolean variable to hold the 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 could be to call mFab.setVisibility(View.VISIBLE) twice, as in the code below. But I did not test 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) { } }); 
0
source

Source: https://habr.com/ru/post/1234262/


All Articles