Condición de carrera al mostrar y ocultar FAB con AnimationUtils

En una aplicación de Android que permite acceder a los usuarios a través de las redes sociales, muestro y oculto un FAB utilizando el siguiente código:

Captura de pantalla de la aplicación

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

Esto funciona bien, cuando llamo el método showFab anterior showFab suficientemente lento.

Antes de iniciar cualquier animación, compruebo la visibilidad actual de FloatingActionButton , de modo que la animación se reproduzca solo una vez , incluso si llamo por ejemplo showFab(true) varias veces en una fila.

Mi problema:

Cuando se muestra un LoginFragment en mi aplicación, primero envío una solicitud a un ServiceIntent para obtener datos de usuario de SQLite y llamar al método siguiente para establecer mi interfaz de usuario en un estado de "espera":

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

Casi inmediatamente una respuesta de SQLite vuelve – vía un LocalBroadcastManager y llamo el método antedicho otra vez: setBusy(false) .

Y entonces el error ocurre y el FAB no es visible.

Si reemplazo el método FAB por código sin animación todo funciona bien:

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

Pero con la animación – una condición de carreras parece ocurrir.

Como solución, he intentado cancelar ambas animaciones, pero esto no ayuda:

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

Por favor sugiera lo que podría hacerse aquí.

He avanzado a través de mi aplicación en depurador numerosas veces ya. El setBusy (y showFab ) se llaman sólo dos veces cuando se muestra el fragmento, pero ambas llamadas ocurren muy rápidamente – y la FAB no se muestra –

Primer intento:

1ª carrera

Segunda carrera:

2ª carrera

ACTUALIZAR:

Desafortunadamente, la synchronized del método no ayuda tampoco: el FAB permanece oculto:

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

Cada una de sus animaciones se ejecutan en hilos separados. Esto conduce a una condición de carrera como usted dijo.

Aquí está lo que está pasando: showFab se dispara con true, luego falso.

  • El método mShowFab onAnimationStart se ejecuta y establece el FAB en visible.
  • El método mHideFab onAnimationStart se ejecuta y no hace nada.
  • El método mShowFab onAnimationEnd se ejecuta y no hace nada.
  • El método mHideFab onAnimationEnd ejecuta y establece el FAB como invisible.
  • Y usted acaba con un FAB invisible que debe ser visible.

Las variables de su segunda ejecución muestran que la animación mShowFab nunca se ejecutará.

Usted debe ser capaz de resolver la condición de carrera escuchando para el final de la animación de ocultar. Algo como esto:

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

La primera cosa a señalar es que no hay nada como la condición de carreras. La terminología correcta es sólo condición de carrera . Tenga esto en cuenta para el futuro 😉

Supongo que está utilizando un BroadcastReceiver para recibir mensajes y que ha creado el objeto y decirle que se ejecute en un hilo separado debido a la animación. Esto significa que su método showFab puede ser llamado dos veces en una sola vez. Para manejar esto, defina el método como sincronizado .

Aquí está mi propia solución –

En primer lugar, el uso de synchronized no ayuda aquí, ya que las animaciones se ejecutan en el mismo subproceso.

Mi solución ha sido introducir una variable boolean separada para mantener el estado FAB final deseado (visible o no 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; } 

Además, creo que otra posibilidad podría haber sido llamar a mFab.setVisibility(View.VISIBLE) dos veces – como en el siguiente código. Pero no lo he probado realmente:

  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) { } }); 
  • Snackbar oculta la acción flotante al presionar el botón de acción en ella
  • Null Pointer Exception on child en onMeasure (): futuresimple / floating-action-button
  • Bloqueo de sombra de botón de acción flotante en los márgenes de vista
  • Diseño de Android
  • ¿Existe un componente nativo para el botón de acción Flotante en Diseño de materiales de Android?
  • Biblioteca de diseño de Android - Problemas de relleno / margen de botón de acción flotante
  • Ajustar el tamaño del icono del botón de acción flotante (fab)
  • Anclaje de ImageView a Collapsing Toolbar
  • Botón de acción flotante en Xamarin.Forms
  • No se pudo encontrar: com.android.support:appcompat-v7:22.2.0
  • Cómo eliminar la sombra circundante FloatingActionButton?
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.