Jérémy Le Piolet - Blog

BottomSheetDialogFragment et keyboard

Petit souci que j’ai rencontré dernièrement avec un BottomSheetDialogFragment, un EditText et le clavier Android.

Problématique

L’idée est de proposer une popup de partage par mail. Pour que ce soit plus sympa qu’une bête fenêtre centrée, j’ai utilisé un BottomSheetDialogFragment dans lequel j’ai mis un layout custom avec un EditText. L’idée est, lorsqu’on appuie sur le bouton d’ouverture de la popup, que le focus soit donné directement à l’EditText avec le clavier ouvert, la popup au dessus.

Affichage désiré du clavier sur BottomSheetDialogFragment
Affichage désiré du clavier sur BottomSheetDialogFragment

Mais bien évidemment, ça ne fonctionnait pas. Soit le clavier ne s’affichait pas et il était nécessaire pour l’utilisateur d’appuyer explicitement sur l’EditText pour le voir, soit le clavier masquait en partie la popup.

Mauvais affichage du clavier sur BottomSheetDialogFragment
Mauvais affichage du clavier sur BottomSheetDialogFragment
Pas d'affichage du clavier sur BottomSheetDialogFragment
Pas d’affichage du clavier sur BottomSheetDialogFragment

Solution

Après avoir un peu galéré, j’ai fini par obtenir un résultat satisfaisant avec le fragment suivant :

public class SendBottomSheetFragment extends BottomSheetDialogFragment {

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_send, container, false);
    }

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

        //Permet de redimensionner la fenêtre quand le clavier apparaît.
        //Si le mode n'est pas activé, le clavier n'est pas visible.
        dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);

        //Traitement à effectuer une fois le dialog affiché
        dialog.setOnShowListener(new DialogInterface.OnShowListener() {
            @Override
            public void onShow(DialogInterface dialogInterface) {

                Handler handler = new Handler();
                handler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        //Récupération du container parent qui gère le comportement du BottomSheetDialog
                        // /!\ l'id peut changer dans les versions futures de la librairie de support
                        BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialog;
                        FrameLayout bottomSheet = (FrameLayout) bottomSheetDialog.findViewById(android.support.design.R.id.design_bottom_sheet);

                        //Force l'ouverture maximale de la bottomSheet
                        BottomSheetBehavior.from(bottomSheet).setState(BottomSheetBehavior.STATE_EXPANDED);
                    }
                }, 250); //Délai pour être sûr que le clavier est bien affiché lorsque l'on demandera l'ouverture maximale.


            }
        });

    }
}

Et le reste du code qui va bien :

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="10dp">

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Bla bla bla"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginTop="20dp"/>

        <EditText
            android:id="@+id/editText"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:hint="email"
            android:inputType="textEmailAddress"
            android:imeOptions="actionNext"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView"
            android:layout_marginTop="10dp"/>


        <Button
            android:id="@+id/route.send.send"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Envoyer"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/editText"
            android:layout_marginTop="10dp"
            android:layout_marginRight="20dp"
            android:layout_marginEnd="20dp"
            android:background="@color/colorAccent"
            android:textColor="@android:color/white"/>

    </android.support.constraint.ConstraintLayout>
</android.support.v4.widget.NestedScrollView>
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button openButton = (Button) findViewById(R.id.button);
        openButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //Affichage de la bottom sheet au clic sur bouton.
                BottomSheetDialogFragment bottomSheetDialogFragment = new SendBottomSheetFragment();
                bottomSheetDialogFragment.show(getSupportFragmentManager(), bottomSheetDialogFragment.getTag());
            }
        });
    }
}
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Push Me"/>

</android.support.constraint.ConstraintLayout>

À noter plusieurs points :

  • Attention à la pérennité : l’id récupéré correspond bien pour le moment à la version de la support librairie utilisée (la version 26.0.0) mais ça peut évoluer ensuite.
  • Il manque des contrôles de nullité des objets, notamment sur le getWindow() (je n’ai pas voulu surcharger le code).
  • Le Handler peut être facultatif : sur un code simple, ça fonctionnait sans. Le délai est également arbitraire et correspondait à mon cas d’usage. Selon la complexité de l’application, il peut être plus ou moins long (attention à ne pas dépasser 500 ms quand même, c’est trop long autrement).
  • Dans de rares cas, ça ne fonctionnait pas, le clavier masquait toujours en partie la vue. Cela se produisait essentiellement lorsque l’on sortait de veille directement sur l’application, et qu’on appuyait directement sur le bouton. Si l’on attendait quelques secondes, le problème n’avait plus lieu. Je suppose qu’en sortie de veille, le clavier est un peu plus long à apparaître et donc le délai du Handler devait être passé. Dans le cas de mon application, au vue des usages, ce n’était pas forcément problématique. Un contournement possible est d’ajouter un second handler avec un délai plus important (par exemple, 700ms) qui fait la même chose que le premier : si le premier ne fonctionne pas, le second prendra la relève, l’utilisateur aura juste l’impression que ça a mis du temps à se faire (mais ça commence à ressembler à du bricolage).
  • Il est sûrement possible de contourner le problème en gérant tout manuellement via BottomSheetBehavior. Dans mon cas, cette solution a toutefois engendré d’autres complications et je suis revenu au BottomSheetDialogFragment.
  • Testé sur devices de la version 16 à la version 25 du SDK Android.