What you see is what you get when you preview an XML layout file. However, if these layout files are displayed in a dialog, the situation is different. It often takes a lot of effort to get the results we want.

This article shares how to define a BaseDialogFragment to achieve what you see is what you get. At the end of the paper, there is also a practical solution to handle the problems related to the nested Fragment in dialog and status bar.

First we create a DialogFragment

public class MyDialogFragment extends DialogFragment {
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_dialog, container, false); }}Copy the code
<?xml version="1.0" encoding="utf-8"? >
<! --fragment_dialog.xml-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FFFFFF"
    android:gravity="center"
    android:orientation="vertical">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="Hello Dialog"
        android:textSize="32dp" />
</LinearLayout>
Copy the code

We expect the result to be dialog filling the screen and the word Hello dialog centered, but the actual result is:

We set the layout on the root to match_parent, but the result is wrap_content.

As we know, a dialog corresponds to a window, and the window has a magical property: isFloating. When isFloating is true, the width and height of the Dialog contentView is reset to WRap_content, and if not, to match_parent.

Let’s change this value by creating a custom theme for our dialog:

<! -- styles.xml -->
<resources>
    <style name="FullScreenDialog" parent="Theme.AppCompat.Dialog">
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowIsFloating">false</item>
        <item name="android:windowBackground">@android:color/transparent</item>
    </style>
</resources>
Copy the code

Apply this topic in MyDialogFragment

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setStyle(DialogFragment.STYLE_NORMAL, R.style.FullScreenDialog);
}
Copy the code

Run and see:

Sure enough, it was full screen, but there were two problems. First, the status bar was black, and second, the ‘Hello Dialog’ was missing.

Let’s defer the first question while we deal with the second.

Currently, there is an error in the support library that causes the style to not display properly. You can solve this problem by using an Activity’s Inflater by changing the onCreateView method:

public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    return getActivity().getLayoutInflater().inflate(R.layout.fragment_dialog, container, false);
}
Copy the code

The Dialog style is now displayed properly, as detailed in this StackOverflow article

Now let’s change the margin of the root layout to leave some space to show the mask:

<?xml version="1.0" encoding="utf-8"? >
<! --fragment_dialog.xml-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:layout_marginLeft="32dp"
    android:layout_marginRight="32dp"
    android:layout_gravity="center"
    android:background="#FFFFFF"
    android:gravity="center"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="Hello Dialog"
        android:textSize="32dp" />

</LinearLayout>
Copy the code

Run and see, the results are disappointing:

Layout_height is not 200dp, but match_parent, which is closely related to the isFloating attribute.

One solution we’ve come up with is to add another layer of FrameLayout to the LinearLayout

<?xml version="1.0" encoding="utf-8"? >
<! --fragment_dialog.xml-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout_gravity="center"
        android:layout_marginLeft="32dp"
        android:layout_marginRight="32dp"
        android:background="#FFFFFF"
        android:gravity="center"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="Hello Dialog"
            android:textSize="32dp" />

    </LinearLayout>
</FrameLayout>
Copy the code

Here we have the desired effect:

But clicking on the mask, the Dialog doesn’t go away, because the dialog is actually full-screen, and there’s no outside to click on.

Now let’s wrap our BaseDialogFragment to solve the following problems:

  1. There is no need to add a layer of FrameLayout to the normal layout
  2. Click on the mask and the Dialog disappears
  3. Fixed black status bar issue

Define DialogFrameLayout to handle clicking masks

public class DialogFrameLayout extends FrameLayout {

    interface OnTouchOutsideListener {
        void onTouchOutside(a);
    }

    GestureDetector gestureDetector = null;

    OnTouchOutsideListener onTouchOutsideListener;

    public void setOnTouchOutsideListener(OnTouchOutsideListener onTouchOutsideListener) {
        this.onTouchOutsideListener = onTouchOutsideListener;
    }

    public DialogFrameLayout(@NonNull Context context) {
        super(context);
        commonInit(context);
    }

    private void commonInit(@NonNull Context context) {
        gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) {
                return true;
            }

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                Rect rect = new Rect();
                getHitRect(rect);
                int count = getChildCount();
                for (int i = count - 1; i > -1; i--) {
                    View child = getChildAt(i);
                    Rect outRect = new Rect();
                    child.getHitRect(outRect);
                    if (outRect.contains((int) e.getX(), (int) e.getY())) {
                        return false; }}if(onTouchOutsideListener ! =null) {
                    onTouchOutsideListener.onTouchOutside();
                }
                return true; }}); }@Override
    public boolean onTouchEvent(MotionEvent event) {
        returngestureDetector.onTouchEvent(event); }}Copy the code

Defining DialogLayoutInflater allows us to eliminate the need for additional FrameLayout

public class DialogLayoutInflater extends LayoutInflater {

    private LayoutInflater layoutInflater;

    private DialogFrameLayout.OnTouchOutsideListener listener;

    public DialogLayoutInflater(Context context, LayoutInflater layoutInflater, DialogFrameLayout.OnTouchOutsideListener listener) {
        super(context);
        this.layoutInflater = layoutInflater;
        this.listener = listener;
    }

    @Override
    public LayoutInflater cloneInContext(Context context) {
        return layoutInflater.cloneInContext(context);
    }

    @Override
    public View inflate(int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        DialogFrameLayout dialogFrameLayout = new DialogFrameLayout(getContext());
        dialogFrameLayout.setOnTouchOutsideListener(listener);
        dialogFrameLayout.setLayoutParams(new ViewGroup.LayoutParams(-1, -1));
        layoutInflater.inflate(resource, dialogFrameLayout, true);
        returndialogFrameLayout; }}Copy the code

Write a BaseDialogFragment to connect everything:

public class BaseDialogFragment extends DialogFragment {

    @NonNull
    @Override
    public LayoutInflater onGetLayoutInflater(@Nullable Bundle savedInstanceState) {
        setStyle(DialogFragment.STYLE_NORMAL, R.style.FullScreenDialog);

        super.onGetLayoutInflater(savedInstanceState);
        // Replace the Activity inflater with an Inflater to fix the fragment style bug
        LayoutInflater layoutInflater = getActivity().getLayoutInflater();
        if(! getDialog().getWindow().isFloating()) { setupDialog(); layoutInflater =new DialogLayoutInflater(requireContext(), layoutInflater,
                    new DialogFrameLayout.OnTouchOutsideListener() {
                        @Override
                        public void onTouchOutside(a) {
                            if(isCancelable()) { dismiss(); }}}); }return layoutInflater;
    }

    protected void setupDialog(a) {
        Window window = getDialog().getWindow();
        // Fix the black status bar issue
        AppUtils.setStatusBarTranslucent(window, true);
        AppUtils.setStatusBarColor(window, Color.TRANSPARENT, false);

        window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
        getDialog().setOnKeyListener(new DialogInterface.OnKeyListener() {
            @Override
            public boolean onKey(DialogInterface dialogInterface, int keyCode, KeyEvent event) {
                if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
                    if (isCancelable()) {
                        dismiss();
                    }
                    return true;
                }
                return false; }}); }}Copy the code

In this way, a BaseDialogFragment encapsulation, MyDialogFragment inheritance BaseDialogFragment, you can achieve what you see is what you get.

public class MyDialogFragment extends BaseDialogFragment {
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        // Note that getActivity().getLayoutInflater() is no longer needed because the BaseDialogFragment already returns the correct inflater
        return inflater.inflate(R.layout.fragment_dialog, container, false); }}Copy the code

Layout files no longer need a FrameLayout

<?xml version="1.0" encoding="utf-8"? >
<! --fragment_dialog.xml-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:layout_gravity="center"
    android:layout_marginLeft="32dp"
    android:layout_marginRight="32dp"
    android:background="#FFFFFF"
    android:gravity="center"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="Hello Dialog"
        android:textSize="32dp" />

</LinearLayout>
Copy the code

As expected, everything is simple, just focus on the layout. But we can go further:

When the Fragment root layout has a layout_gravity=”bottom” attribute, the slide animation is automatically attached:

Status bar variety and Fragment nesting

See AndroidNavigation for details. The library not only handles dialogs, but also fragments nesting, lazy loading of nested fragments, right-swiping back, immersive status bars, toolbars, etc., so you can focus on the business without worrying about application-level UI issues like navigation.