DialogFragment細枝末節

hyyaoming發表於2019-03-10

DialogFragment細枝末節

前言

在Android中,建立對話方塊有兩種,一種是Dialog,另外一種則是今天的主題,官方推薦的DialogFragmet,關於Dialog的使用就不贅述了,今天主要介紹DialogFragment的使用以及一些需要注意的事項,主要有以下幾點。

  • DialogFragmeng相比Dialog的優勢。

  • DialogFrargment的使用。

  • DialogFragment簡單原始碼分析。

  • DialogFragment需要注意的一些小細節。

一、DialogFragment的優勢

  • 在 DialogFragment之前,我們建立對話方塊一般採用 Dialog,而且從程式碼的編寫角度來看,Dialog 使用起來其實更加簡單,但是 Google 卻是推薦儘量使用 DialogFragment,是不是感覺很奇怪,其實原因也很簡單, DialogFragment 有著 Dialog 所沒有的非常好的特性,可以讓它具有更高的可複用性(降低耦合)和更好的便利性(很好的處理螢幕翻轉的情況)。

二、DialogFragment的使用

  • 方式一:重寫DialogFragment的onCreateDialog方法

  • 方式二:重寫DialogFragment的onCreateView方法

1.方式一具體使用

  • DialogFragment中重寫onCreateDialog方法,返回一個Dialog。
    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Logger.d(TAG, "onCreateDialog");
        AppCompatDialog appCompatDialog = new AppCompatDialog(requireActivity());
        TextView textView = new TextView(requireActivity());
        textView.setText("通過onCreateDialog使用DialogFragment");
        textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 28);
        appCompatDialog.setContentView(textView);
        return appCompatDialog;
    }
複製程式碼
  • 在Activity中使用DialogFragment。
    private void showOnCreateDialogFragment(){
        DialogFragment dialogFragment = new DialogFragment();
        dialogFragment.show(getSupportFragmentManager(), "tag");
    }
複製程式碼

2.方式二具體使用

  • DialogFragment中重寫onCreateView方法,該方法建立的View將會作為Dialog的內容佈局,使用方式則是跟方法一在Activity中方式一致,這裡就不贅述了。
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        Logger.d(TAG, "onCreateView");
        TextView textView = new TextView(requireActivity());
        textView.setText("通過onCreateView使用DialogFragment");
        textView.setGravity(Gravity.CENTER);
        textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 28);
        return textView;
    }
複製程式碼

3.DialogFragment中對Dialog屬性的設定,我們可以在DialogFragment的onStar中來對Dialog的樣式進行設定,有一點需要注意的是,對於Dialog樣式的設定,必須在onCretaeDialog方法後面執行,不然得不到Dialog例項,我們可以列印一下DialogFragment從建立到show出來時執行的具體生命週期方法,具體內容如下:

2019-03-10 14:19:10.971 22626-22626/org.lym.sourcecodeparse D/DialogFragment: onAttach
2019-03-10 14:19:10.971 22626-22626/org.lym.sourcecodeparse D/DialogFragment: onCreate
2019-03-10 14:19:10.972 22626-22626/org.lym.sourcecodeparse D/DialogFragment: onCreateDialog
2019-03-10 14:19:10.994 22626-22626/org.lym.sourcecodeparse D/DialogFragment: onActivityCreated
2019-03-10 14:19:11.186 22626-22626/org.lym.sourcecodeparse D/DialogFragment: onStart
2019-03-10 14:19:11.186 22626-22626/org.lym.sourcecodeparse D/DialogFragment: onResume
複製程式碼
  • 從上面列印的回撥生命週期方法來看,我們是可以在onStart方法中對Dialog進行屬性設定的,具體程式碼如下:
    @Override
    public void onStart() {
        super.onStart();
        Dialog dialog = getDialog();
        Window window = dialog.getWindow();
        if (window == null) {
            return;
        }
        WindowManager.LayoutParams attributes = window.getAttributes();
        //設定Dialog視窗的高度
        attributes.height = WindowManager.LayoutParams.MATCH_PARENT;
        //設定Dialog視窗的寬度
        attributes.width = WindowManager.LayoutParams.MATCH_PARENT;
        //設定Dialog的居中方向
        attributes.gravity = Gravity.CENTER;
        //設定Dialog彈出時背景的透明度
        attributes.dimAmount = 0.6f;
        //設定Dialog水平方向的間距
        attributes.horizontalMargin = 0f;
        //設定Dialog垂直方向的間距
        attributes.verticalMargin = 0f;
        //設定Dialog顯示時X軸的座標,具體螢幕X軸的偏移量
        attributes.x = 0;
        //設定Dialog顯示時Y軸的座標,距離螢幕Y軸的偏移量
        attributes.y = 0;
        //設定Dialog的透明度
        attributes.alpha = 0f;
        //設定Dialog顯示和消失時的動畫
        attributes.windowAnimations = 0;
        window.setAttributes(attributes);
        Logger.d(TAG, "onStart");
    }

複製程式碼

三、DialogFragmeng原始碼分析

1.下面先貼一下DialogFragment的部分原始碼,並進行簡要分析。

public class DialogFragment extends Fragment implements OnCancelListener, OnDismissListener {
    boolean mViewDestroyed;
    boolean mDismissed;
    boolean mShownByMe;

    public DialogFragment() {
    }
    
    //標準的顯示Fragment的方法,沒什麼好說的
    public void show(FragmentManager manager, String tag) {
        this.mDismissed = false;
        this.mShownByMe = true;
        FragmentTransaction ft = manager.beginTransaction();
        ft.add(this, tag);
        ft.commit();
    }
    
    //傳入一個事務管理,將fragment加入到事務管理中,並返回回退棧id
    //返回的mBackStackId將在下文中用到
    public int show(FragmentTransaction transaction, String tag) {
        this.mDismissed = false;
        this.mShownByMe = true;
        transaction.add(this, tag);
        this.mViewDestroyed = false;
        this.mBackStackId = transaction.commit();
        return this.mBackStackId;
    }
    
    //第一個show方法的加強版,看名字就知道,使用這個方法之後,則會立即執行fragment當中相關的方法,這個待會兒作出解釋
    public void showNow(FragmentManager manager, String tag) {
        this.mDismissed = false;
        this.mShownByMe = true;
        FragmentTransaction ft = manager.beginTransaction();
        ft.add(this, tag);
        ft.commitNow();
    }

    //Dialaog消失時的回撥,setOndismissListener是在onActivityCreate中設定的,當前正是把dialog給dismiss,並沒有讓dialogfragment出棧
    public void dismiss() {
        this.dismissInternal(false);
    }
    
    //dismiss的加強版,先消失dialog,並將dialogfragment移除棧內
    public void dismissAllowingStateLoss() {
        this.dismissInternal(true);
    }

    //顯示判斷dialog,是否已經消失,如果已經消失,則不做任何操作
    //1.如果dialog例項不為空,先呼叫dialog的dismiss方法,隱藏dialog
    //2.如果先前呼叫的是public int show(FragmentTransaction transaction, String tag)
    //方法顯示的dialogfragment,那麼此時會根據之前返回的mBackStackId來將fragment移除棧內
    //3.如果不是則再啟用事務將dialogfragment移除棧內,這裡會根據傳入的allowStateLoss來區分
    //提交事務的方法
    void dismissInternal(boolean allowStateLoss) {
        if (!this.mDismissed) {
            this.mDismissed = true;
            this.mShownByMe = false;
            if (this.mDialog != null) {
                this.mDialog.dismiss();
            }

            this.mViewDestroyed = true;
            if (this.mBackStackId >= 0) {
                this.getFragmentManager().popBackStack(this.mBackStackId, 1);
                this.mBackStackId = -1;
            } else {
                FragmentTransaction ft = this.getFragmentManager().beginTransaction();
                ft.remove(this);
                if (allowStateLoss) {
                    ft.commitAllowingStateLoss();
                } else {
                    ft.commit();
                }
            }

        }
    }
    
    //如果子類重寫該方法,那麼使用的就是你自定義的dialog
    @NonNull
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        return new Dialog(this.getActivity(), this.getTheme());
    }
    
    //dialog設定了onDismissListener後的回撥,如果dialog正常消失,次回撥中的方法不會呼叫到
    //因為在DialogFragment的onStar方法中將mViewDestroyed變數賦值為true,dialog顯示設定到呼叫顯示出來的生命週期回撥我們已經列印過了。
    public void onDismiss(DialogInterface dialog) {
        if (!this.mViewDestroyed) {
            this.dismissInternal(true);
        }
    }

    //該方法,先判斷Dialog是否已經顯示,然後會取onCreateView中返回的View,如果View不為空,那麼該View將作為Dialog的內容佈局,所以,如果你同時重寫了onCreateDialog和onCreateView方法,那麼會優先採用onCreateView當中的View作為內容佈局,然後再作了一些監聽設定
    //設定了dialog是否可點選
    //設定了dialog的消失監聽onDismissListener,所以消失時會回撥文中的dimiss方法
    //設定了dialog的取消監聽onCancelListener,在取消時會回撥文中的onCancel方法
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (this.mShowsDialog) {
            View view = this.getView();
            if (view != null) {
                if (view.getParent() != null) {
                    throw new IllegalStateException("DialogFragment can not be attached to a container view");
                }

                this.mDialog.setContentView(view);
            }

            Activity activity = this.getActivity();
            if (activity != null) {
                this.mDialog.setOwnerActivity(activity);
            }

            this.mDialog.setCancelable(this.mCancelable);
            this.mDialog.setOnCancelListener(this);
            this.mDialog.setOnDismissListener(this);
            if (savedInstanceState != null) {
                Bundle dialogState = savedInstanceState.getBundle("android:savedDialogState");
                if (dialogState != null) {
                    this.mDialog.onRestoreInstanceState(dialogState);
                }
            }

        }
    }
}

複製程式碼

2.原始碼分析總結

  • 可以重寫onCreateDialog或者onCreateView來建立DialogFragment的檢視,但是onCretaeView的優先順序要高於onCreateDilaog,所以沒有特殊的需要,一般通過重寫onCreateView來建立Dialog的檢視即可。

  • 從一開始我們就列印了DialogFragment從建立檢視到show出來回撥的一系列生命週期方法,我們得知設定dialog的屬性可以在DialogFragment中的onStar回撥中進行。

  • 對於dialog的顯示我們通過檢視原始碼得知一共有三個方法,show()方法傳入一個fragmentManager以及一個tag,showNow方法,傳入一個fragmentManger和一個tag,show()方法,傳入一個事務和一個tag,並返回該事務中回退棧的id,在呼叫dismissInternal()方法時,會根據顯示DialogFragment的型別來操作DialogFragment出棧。

  • DialogFragment會在onActivityCreate()中對Dialog的內容試圖進行一次賦值,如果你重寫了onCrateView方法,並返回了一個不為空的例項,那麼將會作為該Dialog的內容檢視,然後再對dialog設定了是否可取消,消失監聽,以及取消監聽

  • 通過上面的原始碼簡析,我們得知在DialogFragment消失時,只會將dialog給隱藏,並不會講DialogFragment移除棧內,如果想講DialogFragment在dialog消失時移除棧內,那麼需要手動呼叫dismissInternal()方法

四、細節注意事項

  • 可以通過重寫onCreateDialog和onCreateView來設定DialogFragment的檢視,但onCraeteView設定的檢視優先順序要高於在onCreateDialog。

  • 設定Dialog的屬性需要在onCreateDialog回撥後設定,不然getDialog得到的物件則會是null,推薦在onStar方法中對Dialog進行屬性設定。

  • DialogFragment單個例項只能show一次DialogFrament,如果多次展示的話,則會丟擲如下異常

    Process: org.lym.sourcecodeparse, PID: 27108
    java.lang.IllegalStateException: Fragment already added: DialogFragment{8262d74 #0 tag}
        at android.support.v4.app.FragmentManagerImpl.addFragment(FragmentManager.java:1893)
        at android.support.v4.app.BackStackRecord.executeOps(BackStackRecord.java:760)
        at android.support.v4.app.FragmentManagerImpl.executeOps(FragmentManager.java:2595)
        at android.support.v4.app.FragmentManagerImpl.executeOpsTogether(FragmentManager.java:2382)
        at android.support.v4.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManager.java:2337)
複製程式碼
  • DialogFragment在正常Dismiss後並不會直接從當前的棧中移除,而是在DialogFragment中的onDestroyView()回撥時,才會對DialogFragment進行出棧操作,所以如果你如果需要在Activity中頻繁的顯示隱藏一個DialogFragment,那麼在dismiss時需要手動的呼叫dismissAllowingStateLoss()方法,並且再次show時不能用上一個DialogFragment例項。

  • DialogFragment並沒有對Dialog的消失提供監聽給呼叫者使用,但是我們通過原始碼分析得知,DialogFragment在onActivityCreate當中其實已經幫我們設定了onDismissListener,所以我們如果需要對Dialog的消失進行監聽的話只需重寫onDismiss方法即可,還有一種方式則是覆蓋父類設定的onDismissListener監聽,但是需注意的時,這個監聽的複寫,必須在父類onActivityCreate方法呼叫之後,關於消失監聽的兩種寫法如下:

//mListener為提供到外部使用的回撥
 @Override
    public void onDismiss(DialogInterface dialog) {
        super.onDismiss(dialog);
        mListener.onDismiss(dialog);
        Logger.d(TAG, "onDismiss");
    }
    
    //複寫setOnDismissListener必須發生在父類的onActivityCreate之後
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (getDialog() != null && null != mListener) {
            getDialog().setOnDismissListener(new DialogInterface.OnDismissListener() {
                @Override
                public void onDismiss(DialogInterface dialog) {
                    ToastUtils.showToast("覆蓋後的OnDismiss Listener");
                }
            });
        }
        Logger.d(TAG, "onActivityCreated");
    }
複製程式碼

結語:以上便是全文的內容,是自己在使用DialogFragment中碰到了一些坑以及學習後的一些理解,希望能對您有幫助。

相關文章