¿Cómo mantengo el modo inmersivo en los diálogos?

104

¿Cómo mantengo el nuevo modo inmersivo cuando mis actividades muestran un diálogo personalizado?

Estoy usando el código siguiente para mantener el modo inmersivo en los cuadros de diálogo, pero con esa solución, la barra de navegación aparece durante menos de un segundo cuando inicio mi cuadro de diálogo personalizado y luego desaparece.

El siguiente video explica mejor el problema (mire la parte inferior de la pantalla cuando aparezca la barra de navegación): http://youtu.be/epnd5ghey8g

¿Cómo evito este comportamiento?

CÓDIGO

El padre de todas las actividades en mi aplicación:

public abstract class ImmersiveActivity extends Activity {

    @SuppressLint("NewApi")
    private void disableImmersiveMode() {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
            getWindow().getDecorView().setSystemUiVisibility(
                    View.SYSTEM_UI_FLAG_FULLSCREEN
            );
        }
    }

    @SuppressLint("NewApi")
    private void enableImmersiveMode() {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
            getWindow().getDecorView().setSystemUiVisibility(
                      View.SYSTEM_UI_FLAG_LAYOUT_STABLE 
                    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 
                    | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 
                    | View.SYSTEM_UI_FLAG_FULLSCREEN 
                    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY 
                    | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
            );
        }
    }


    /**
     * Set the Immersive mode or not according to its state in the settings:
     * enabled or not.
     */
    protected void updateSystemUiVisibility() {
        // Retrieve if the Immersive mode is enabled or not.
        boolean enabled = getSharedPreferences(Util.PREF_NAME, 0).getBoolean(
                "immersive_mode_enabled", true);

        if (enabled) enableImmersiveMode();
        else disableImmersiveMode();
    }

    @Override
    public void onResume() {
        super.onResume();
        updateSystemUiVisibility();
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        updateSystemUiVisibility();
    }

}


Todos mis cuadros de diálogo personalizados llaman a este método en su onCreate(. . .)método:

/**
 * Copy the visibility of the Activity that has started the dialog {@link mActivity}. If the
 * activity is in Immersive mode the dialog will be in Immersive mode too and vice versa.
 */
@SuppressLint("NewApi")
private void copySystemUiVisibility() {
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
        getWindow().getDecorView().setSystemUiVisibility(
                mActivity.getWindow().getDecorView().getSystemUiVisibility()
        );
    }
}


EDITAR - LA SOLUCIÓN (gracias a Beaver6813, busque su respuesta para obtener más detalles):

Todos mis cuadros de diálogo personalizados anulan el método show de esta manera:

/**
 * An hack used to show the dialogs in Immersive Mode (that is with the NavBar hidden). To
 * obtain this, the method makes the dialog not focusable before showing it, change the UI
 * visibility of the window like the owner activity of the dialog and then (after showing it)
 * makes the dialog focusable again.
 */
@Override
public void show() {
    // Set the dialog to not focusable.
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);

    copySystemUiVisibility();

    // Show the dialog with NavBar hidden.
    super.show();

    // Set the dialog to focusable again.
    getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
}
VanDir
fuente
¿Cómo se muestran los diálogos? ¿Usas DialogFragments?
Tadeas Kriz
No uso DialogFragments sino subclases de diálogo personalizadas. developer.android.com/reference/android/app/Dialog.html Muestro los diálogos simplemente llamando a su método show ().
VanDir
Cuando aparece el diálogo, se llama a onWindowFocusChanged. ¿Cuál es el valor de habilitado cuando aparece el diálogo? ¿Es cierto o algo salió mal y es falso?
Kevin van Mierlo
¿Te refieres al método onWindowFocusChanged (boolean hasFocus) de la clase Dialog (y no de la clase Activity)? En este caso, la bandera "hasFocus" es verdadera.
VanDir
5
¿Alguien usó el modo inmersivo con fragmentos de diálogo?
gbero

Respuestas:

167

Después de mucha investigación sobre el problema, hay una solución hacky para esto, que involucró destrozar la clase Dialog para encontrar. La barra de navegación se muestra cuando la ventana de diálogo se agrega al Administrador de ventanas, incluso si configura la visibilidad de la interfaz de usuario antes de agregarla al administrador. En el ejemplo de Android Immersive se comenta que:

// * Uses semi-transparent bars for the nav and status bars
// * This UI flag will *not* be cleared when the user interacts with the UI.
// When the user swipes, the bars will temporarily appear for a few seconds and then
// disappear again.

Creo que eso es lo que estamos viendo aquí (que se activa una interacción del usuario cuando se agrega una nueva vista de ventana enfocable al administrador).

¿Cómo podemos solucionar esto? Hacer que el diálogo no se pueda enfocar cuando lo creamos (para que no activemos una interacción del usuario) y luego hacer que se pueda enfocar después de que se muestre.

//Here's the magic..
//Set the dialog to not focusable (makes navigation ignore us adding the window)
dialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);

//Show the dialog!
dialog.show();

//Set the dialog to immersive
dialog.getWindow().getDecorView().setSystemUiVisibility(
context.getWindow().getDecorView().getSystemUiVisibility());

//Clear the not focusable flag from the window
dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);

Claramente, esto no es ideal, pero parece ser un error de Android, deberían verificar si la ventana tiene un conjunto inmersivo.

Actualicé mi código de prueba de trabajo (perdona el desorden hacky) a Github . Probé en el emulador de Nexus 5, probablemente explote con algo menos que KitKat, pero es solo para prueba de concepto.

Castor6813
fuente
3
Gracias por el proyecto de muestra, pero no funcionó. Mira lo que sucede en mi Nexus 5: youtu.be/-abj3V_0zcU
VanDir
Actualicé mi respuesta después de investigar un poco el problema, ¡fue difícil! Probado en un emulador de Nexus 5, con suerte este funciona.
Beaver6813
2
Recibo 'requestFeature () debe ser llamado antes de agregar contenido' (creo que depende de las características activas de la actividad). Solución: Mueva dialog.show () una línea hacia arriba para que se invoque show () antes de copiar SystemUiVisibility (pero después de configurar no enfocable). Entonces todavía funciona sin saltar la barra de navegación.
arberg
5
@ Beaver6813 no funciona cuando se usa EditText y aparece un teclado en DialogFragment. ¿Tiene alguna sugerencia para manejarlo?
androidcodehunter
1
Hola Beaver6813. No funciona cuando tengo un texto de edición en el diálogo. ¿¿Hay alguna solución??
Navin Gupta
33

Para su información, gracias a la respuesta de @ Beaver6813, he podido hacer que esto funcione usando DialogFragment.

en el método onCreateView de mi DialogFragment, acabo de agregar lo siguiente:

    getDialog().getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
    getDialog().getWindow().getDecorView().setSystemUiVisibility(getActivity().getWindow().getDecorView().getSystemUiVisibility());

    getDialog().setOnShowListener(new DialogInterface.OnShowListener() {
        @Override
        public void onShow(DialogInterface dialog) {
            //Clear the not focusable flag from the window
            getDialog().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);

            //Update the WindowManager with the new attributes (no nicer way I know of to do this)..
            WindowManager wm = (WindowManager) getActivity().getSystemService(Context.WINDOW_SERVICE);
            wm.updateViewLayout(getDialog().getWindow().getDecorView(), getDialog().getWindow().getAttributes());
        }
    });
gbero
fuente
5
¿Funciona esto con el patrón Builder en onCreateDialog ()? Todavía tengo que hacer que este truco funcione con mis DialogFragments, el diálogo bloquea mi aplicación con "requestFeature () debe ser llamado antes de agregar contenido"
IAmKale
1
Esto es brillante y fue la única solución para mi uso de DialogFragments. Muchas gracias.
se22a
¡Excelente! Funciona perfectamente. Probé tantas cosas, ahora perfecto continúa en modo inmersivo.
Peterdk
wow, trabajando como un encanto. gracias
Abdul
16

Si desea utilizar onCreateDialog () , pruebe esta clase. Funciona bastante bien para mí...

public class ImmersiveDialogFragment extends DialogFragment {

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {

        AlertDialog alertDialog = new AlertDialog.Builder(getActivity())
                .setTitle("Example Dialog")
                .setMessage("Some text.")
                .create();

        // Temporarily set the dialogs window to not focusable to prevent the short
        // popup of the navigation bar.
        alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);

        return alertDialog;

    }

    public void showImmersive(Activity activity) {

        // Show the dialog.
        show(activity.getFragmentManager(), null);

        // It is necessary to call executePendingTransactions() on the FragmentManager
        // before hiding the navigation bar, because otherwise getWindow() would raise a
        // NullPointerException since the window was not yet created.
        getFragmentManager().executePendingTransactions();

        // Hide the navigation bar. It is important to do this after show() was called.
        // If we would do this in onCreateDialog(), we would get a requestFeature()
        // error.
        getDialog().getWindow().getDecorView().setSystemUiVisibility(
            getActivity().getWindow().getDecorView().getSystemUiVisibility()
        );

        // Make the dialogs window focusable again.
        getDialog().getWindow().clearFlags(
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
        );

    }

}

Para mostrar el cuadro de diálogo, haga lo siguiente en su actividad ...

new ImmersiveDialogFragment().showImmersive(this);
2r4
fuente
¿Alguna idea sobre cómo detener la visualización de la barra de navegación cuando se muestra un menú de la barra de acciones?
William
Todavía obtengo NPE, porque getDialog () devuelve nulo. ¿Cómo lograste eso?
Anton Shkurenko
8

Combinando las respuestas aquí hice una clase abstracta que funciona en todos los casos:

public abstract class ImmersiveDialogFragment extends DialogFragment {

    @Override
    public void setupDialog(Dialog dialog, int style) {
        super.setupDialog(dialog, style);

        // Make the dialog non-focusable before showing it
        dialog.getWindow().setFlags(
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
    }

    @Override
    public void show(FragmentManager manager, String tag) {
        super.show(manager, tag);
        showImmersive(manager);
    }

    @Override
    public int show(FragmentTransaction transaction, String tag) {
        int result = super.show(transaction, tag);
        showImmersive(getFragmentManager());
        return result;
    }

    private void showImmersive(FragmentManager manager) {
        // It is necessary to call executePendingTransactions() on the FragmentManager
        // before hiding the navigation bar, because otherwise getWindow() would raise a
        // NullPointerException since the window was not yet created.
        manager.executePendingTransactions();

        // Copy flags from the activity, assuming it's fullscreen.
        // It is important to do this after show() was called. If we would do this in onCreateDialog(),
        // we would get a requestFeature() error.
        getDialog().getWindow().getDecorView().setSystemUiVisibility(
                getActivity().getWindow().getDecorView().getSystemUiVisibility()
        );

        // Make the dialogs window focusable again
        getDialog().getWindow().clearFlags(
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
        );
    }
}
4emodan
fuente
Dado que androidx, el método setupDialog se convirtió en una API restringida, ¿cómo podríamos lidiar con eso excepto ignorarlo?
Mingkang Pan
5

Esto también funciona sobre el método onDismiss de su fragmento de diálogo. Y dentro de ese método, llame al método de la actividad a la que está adjunto, establezca nuevamente los indicadores de pantalla completa.

@Override
    public void onDismiss(DialogInterface dialog) {
        super.onDismiss(dialog);
        Logger.e(TAG, "onDismiss");
        Log.e("CallBack", "CallBack");
        if (getActivity() != null &&
                getActivity() instanceof LiveStreamingActivity) {
            ((YourActivity) getActivity()).hideSystemUI();
        }
    }

Y en tu actividad agrega este método:

public void hideSystemUI() {
        // Set the IMMERSIVE flag.
        // Set the content to appear under the system bars so that the content
        // doesn't resize when the system bars hide and show.
        getWindow().getDecorView().setSystemUiVisibility(
                View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                        | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                        | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
                        | View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar
                        | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
    }
Salil Kaul
fuente
1
Comentario más útil, ya que funciona muy bien con AlertDialog.Buildery.setOnDismissListener()
tomash
4

Cuando está creando su propio DialogFragment, solo necesita anular este método.

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    Dialog dialog = super.onCreateDialog(savedInstanceState);

    dialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);

    return dialog;
}
Dawid Drozd
fuente
No es realmente ideal, ya que no podrá presionar nada dentro del cuadro de diálogo :(
slott
Al usar esto, mis diálogos dejan de responder. Pero si insiste en que funciona, lo intentaré de nuevo.
slott
@slott, debe borrar el WindowManager.LayoutParams.FLAG_NOT_FOCUSABLEcuadro de diálogo posterior que se muestra
4emodan
2

Sé que esta es una publicación antigua, pero mi respuesta puede ayudar a otros.

A continuación se muestra la solución hacky para el efecto inmersivo en los diálogos:

public static void showImmersiveDialog(final Dialog mDialog, final Activity mActivity) {
        //Set the dialog to not focusable
        mDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
        mDialog.getWindow().getDecorView().setSystemUiVisibility(setSystemUiVisibility());

        mDialog.setOnShowListener(new DialogInterface.OnShowListener() {
            @Override
            public void onShow(DialogInterface dialog) {
                //Clear the not focusable flag from the window
                mDialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);

                //Update the WindowManager with the new attributes
                WindowManager wm = (WindowManager) mActivity.getSystemService(Context.WINDOW_SERVICE);
                wm.updateViewLayout(mDialog.getWindow().getDecorView(), mDialog.getWindow().getAttributes());
            }
        });

        mDialog.getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
            @Override
            public void onSystemUiVisibilityChange(int visibility) {
                if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
                    mDialog.getWindow().getDecorView().setSystemUiVisibility(setSystemUiVisibility());
                }

            }
        });
    }

    public static int setSystemUiVisibility() {
        return View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_FULLSCREEN
                | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
    }
Rahul Parihar
fuente