底部彈出式選單, 可以使用PopupWindow來做,也可以用自定義View來做。當然這裡採用DialogFragment來做。

DialogFragment是3.0之後引入的,使用DialogFragment,我們不用管理其生命週期,並且可以作為元件重用。比如當螢幕旋轉的時候,如果PopupWindow沒有dismiss掉,會丟擲異常。AlertDialog則會消失,DialogFragment建立的對話方塊則不受影響。

概述

使用DialogFragment,需要重寫onCreateView或者onCreateDialog方法,前者是通過layout下的自定義佈局來建立對話方塊,後者則是用AlertDialog或者Dialog創建出Dialog,適用於建立簡單的對話方塊。

如果同時複寫onCreateViewonCreateDialog會報如下異常,

AndroidRuntimeException: requestFeature() must be called before adding content

通過檢視DialogFragment的原始碼,我們發現會有下面的註釋

     * This method will be called after {@link #onCreate(Bundle)} and
     * before {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.  The
     * default implementation simply instantiates and returns a {@link Dialog}
     * class.

You can override both (in fact the DialogFragment says so), the problem comes when you try to inflate the view after having already creating the dialog view. You can still do other things in onCreateView, like use the savedInstanceState, without causing the exception.

可以看出onCreateDialog優先於onCreateView執行,如果我們複寫了這兩個方法,那麼對話方塊是在onCreateDialog中建立的,但是我們依然可以在onCreateView中做狀態儲存等操作。

onCreateDialog

這個回撥方法是DialogFragment獨有的,通過它返回的是一個Dialog物件,這個物件就會被顯示到螢幕上。

AlertDialog.Builder builder = new AlertDialog.Builder(this);
                builder.setView(R.layout.dialog_fragment_item);
                builder.setPositiveButton("確定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                });
                builder.show();

Activity中我們可以通過如下兩種方式將對話方塊展示出來

 BottomDialogFragment bottomDialogFragment = (BottomDialogFragment) Fragment.instantiate(this, BottomDialogFragment.class.getName());
                getSupportFragmentManager().beginTransaction().add(bottomDialogFragment, "bottomDialogFragment").commitAllowingStateLoss();
                break;

或者:

BottomDialogFragment dialog = new BottomDialogFragment();  
        dialog.show(getSupportFragmentManager(), "bottomDialogFragment");

onCreateView

通過onCreateView自定義佈局展示對話方塊。生命週期同Fragment,同時,支援FragmentManager 事務

  • 佈局檔案:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="30dp"
    android:orientation="horizontal">

    <ImageView
        android:id="@android:id/icon"
        android:layout_width="50dp"
        android:layout_height="match_parent"
        android:adjustViewBounds="true"
        android:padding="5dp"
        android:src="@mipmap/ic_launcher" />

    <TextView
        android:id="@android:id/text1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ellipsize="marquee"
        android:padding="10dp"
        android:singleLine="true"
        android:text="@string/image_content"
        android:textSize="15sp" />
</LinearLayout>
  • java程式碼,
    繼承DialogFragment,重寫onCreateView方法
@Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        if (null == fragmentRoot) {
            fragmentRoot = inflater.inflate(R.layout.dialog_fragment_item, container, false);
        }

        if (null != fragmentRoot) {
            ViewGroup parent = (ViewGroup) fragmentRoot.getParent();
            if (null != parent)
                parent.removeAllViews();
        }

        return fragmentRoot;
    }

去標題

去標題有兩種方式,通過 程式碼 或者 通過 theme

  • 程式碼設定,需要用到DialogFragment.STYLE_NO_TITLE

  • 主題設定,需要在style.xml中使用NoActionBar屬性。

 <item name="windowNoTitle">true</item>

注意

一. 如果使用onCreateDialog 建立對話方塊時,可以通過如下方式設定style

AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.BottomDialog);

同時,可以在onCreateDialog 通過Dialog 物件獲取Window 物件,並設定相關屬性

dialog = builder.create();
Window window = dialog.getWindow();

二. 如果使用 onCreateView 建立對話方塊,則設定 style 的方式將有所變化.

  • 必須在onCreateView 中獲取window 物件,並設定Window 的相關屬性,在onCreate 中設定無效
 getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);
  • 必須在onCreate 中設定Style ,而在OnCreateView 中設定無效,因為此時對話方塊已經init
 setStyle(DialogFragment.STYLE_NO_TITLE, R.style.BottomDialog);

位置控制

像很多UI都採用底部彈出的效果,比如展示選單,分享等操作,我們都知道Dialog是展示在螢幕中央,而且寬度是沒有填充螢幕的,其實我們只要給dialog設定一個LayoutParams即可。

ps:

 @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {

        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.BottomDialog);
        LayoutInflater inflater = getActivity().getLayoutInflater();
        View view = inflater.inflate(R.layout.dialog_fragment_layout, null);

        initView(view);

        builder.setView(view);

        dialog = builder.create();

        dialog.setCanceledOnTouchOutside(true);

        // 設定寬度為屏寬、靠近螢幕底部。
        Window window = dialog.getWindow();
        WindowManager.LayoutParams wlp = window.getAttributes();
        wlp.gravity = Gravity.BOTTOM;
        window.setAttributes(wlp);

        return dialog;
    }

上面是通過設定了Gravity.BOTTOM來實現在螢幕下方顯示。預設情況下DialogFragment是現實在螢幕中間的,我們如果想要改變其現實位置,同理,也可以用此方法。

比如,在螢幕中間靠上顯示,可以這樣設定。

        Window window = dialog.getWindow();
        WindowManager.LayoutParams wlp = window.getAttributes();
        wlp.gravity = Gravity.TOP;
        // 這裡是座標值,即離螢幕上方距離是100
        wlp.y = 100;
        window.setAttributes(wlp);

我們發現在DialogFragment 彈出的時候,左右兩邊會留白,這是所有Dialog
都有的,本來試過通過LayoutParams控制,當時失敗,之後找到了解決辦法
相關程式碼

@Override public void onStart() {
        super.onStart();
        Dialog dialog = getDialog();
        if (null != dialog) {
            dialog.getWindow().setLayout(-1, -2);
        }
    }

DialogFragment動畫

這裡涉及到了一個theme,主要是設定動畫的,dialog自下而上的彈出來

<!--螢幕底部的dialog-->
    <style name="BottomDialog" parent="AppTheme">
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowIsFloating">true</item>
        <!-- Dialog進入及退出動畫 -->
        <item name="android:windowAnimationStyle">@style/BottomToTopAnim</item>

    </style>
<style name="BottomToTopAnim" parent="android:Animation">
        <item name="@android:windowEnterAnimation">@anim/bottomview_anim_enter</item>
        <item name="@android:windowExitAnimation">@anim/bottomview_anim_exit</item>
    </style>

enter動畫:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromYDelta="100%p"
        android:toYDelta="0%p" />

    <alpha
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromAlpha="0.0"
        android:toAlpha="1.0" />
</set>

exit動畫:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromYDelta="0%p"
        android:toYDelta="100%p" />

    <alpha
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromAlpha="1.0"
        android:toAlpha="0.3" />
</set>

防止重複彈出

我們都知道,如果我們點選一個按鈕彈出一個對話方塊,如果每次都是新建Dialog ,則會出現重疊現象,在FragmentManager中有一個方法isAdded()可以用來判斷此 Dialog是否被新增,同時,為了減少相同Dialog 的建立,我們並不需要每次都new 一個出來,並通過isAdded()來判斷是否新增,減少不必要的消耗

 public static BottomDialogFragment showDialog(AppCompatActivity appCompatActivity) {
        FragmentManager fragmentManager = appCompatActivity.getSupportFragmentManager();
        BottomDialogFragment bottomDialogFragment =
            (BottomDialogFragment) fragmentManager.findFragmentByTag(TAG);
        if (null == bottomDialogFragment) {
            bottomDialogFragment = newInstance();
        }

        if (!appCompatActivity.isFinishing()
            && null != bottomDialogFragment
            && !bottomDialogFragment.isAdded()) {
            fragmentManager.beginTransaction()
                .add(bottomDialogFragment, TAG)
                .commitAllowingStateLoss();
        }

        return bottomDialogFragment;
    }