Android分析DialogFragment源碼

原文地址:www.jianshu.com/p/ea0630a6c…java

一. DialogFragment源碼分析。

由於是Fragment,咱們先從onCreate生命週期入手。android

@Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 通常這樣設置樣式
        setStyle(.....);
    }
複製代碼

先來DialogFragment中有個style方法編程

public void setStyle(int style, @StyleRes int theme) {
        this.mStyle = style;
        if (this.mStyle == 2 || this.mStyle == 3) {
            this.mTheme = 16973913;
        }

        if (theme != 0) {
            this.mTheme = theme;
        }

    }
複製代碼

能夠看出這裏並無作什麼操作,只是把傳進來的style和theme存到全局變量。 由於fragment不會平白無故去走他的生命週期方法,因此入口方法就是show()方法。安全

public void show(FragmentManager manager, String tag) {
        this.mDismissed = false;
        this.mShownByMe = true;
        FragmentTransaction ft = manager.beginTransaction();
        ft.add(this, tag);
        ft.commit();
    }
複製代碼

能夠看出這裏先設置了兩個屬性mDismissed和mShownByMe 。很惋惜的是源碼中並無給這兩個屬性添加註釋,那就只能猜了,從命名上猜和編程習慣猜mDismissed是記錄這個Dialog是否dismiss,mShownByMe 是記錄是不是用戶調起的show方法。 而後用manager.beginTransaction()拿到FragmentTransaction,抽象理解就是當前這個外層頁面的FragmentManager的事物,而後把當前fragment添加到外層界面的FragmentManager,commit就是提交。 加了會怎麼樣?那還用說,咱們先看看Activity動態展現Fragment的代碼,我隨便去網上copy一段代碼bash

getSupportFragmentManager()  
                .beginTransaction()
                .add(佈局的ID , fragment)
                .commit();
複製代碼

就都是這樣的操做,因此你說添加了會怎樣,固然是走Fragment的生命週期啊。按照Fragment的生命週期鉤子來走,按順序看看DialogFragment有重寫哪些生命週期方法。多線程

public void onAttach(Context context) {
        super.onAttach(context);
        if (!this.mShownByMe) {
            this.mDismissed = false;
        }

    }
複製代碼

這裏作了一個判斷,應該是爲了安全性考慮,若是這個Dialog不是由咱們調用show方法展現的話,還記得在show方法中有設置this.mDismissed = false;嗎 , 若是不是調用show方法,而恰巧這個Fragment的生命週期又被調用了。因此這裏爲了安全考慮補上this.mDismissed = false。 而後調用onCreateapp

public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.mShowsDialog = this.mContainerId == 0;
        if (savedInstanceState != null) {
            this.mStyle = savedInstanceState.getInt("android:style", 0);
            this.mTheme = savedInstanceState.getInt("android:theme", 0);
            this.mCancelable = savedInstanceState.getBoolean("android:cancelable", true);
            this.mShowsDialog = savedInstanceState.getBoolean("android:showsDialog", this.mShowsDialog);
            this.mBackStackId = savedInstanceState.getInt("android:backStackId", -1);
        }

    }
複製代碼

this.mShowsDialog = this.mContainerId == 0這個容器ID mContainerId 我也不太清楚是什麼,先跳過。 下面 if (savedInstanceState != null) {......} 是就恢復數據的操做。能夠看到官網在onCreate中的恢復數據的寫法是怎麼寫的,十分建議學會使用這種作法,能讓代碼更爲安全。 相應的能夠先來直接看看保存數據的作法ide

public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        if (this.mDialog != null) {
            Bundle dialogState = this.mDialog.onSaveInstanceState();
            if (dialogState != null) {
                outState.putBundle("android:savedDialogState", dialogState);
            }
        }

        if (this.mStyle != 0) {
            outState.putInt("android:style", this.mStyle);
        }

        if (this.mTheme != 0) {
            outState.putInt("android:theme", this.mTheme);
        }

        if (!this.mCancelable) {
            outState.putBoolean("android:cancelable", this.mCancelable);
        }

        if (!this.mShowsDialog) {
            outState.putBoolean("android:showsDialog", this.mShowsDialog);
        }

        if (this.mBackStackId != -1) {
            outState.putInt("android:backStackId", this.mBackStackId);
        }

    }
複製代碼

能夠看到保存fragment的數據以前,先保存dialog的數據。 咱們繼續來看生命週期onActivityCreated源碼分析

public void onActivityCreated(@Nullable 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);
                }
            }

        }
    }
複製代碼

能夠看出這裏就是把view設置給Dialog,而這個view就是咱們在onCreateView方法中所返回的view。因此先前須要判斷view.getParent(),由於一個子view不能同時擁有兩個父view。 this.mDialog.setOwnerActivity(activity);這個好像是把activity傳給Dialog,由於Dialog裏面確定要用到activity的地方。後面的代碼就是設置能關閉,設置Cancel和Dismiss時的監聽,還有獲取savedInstanceState保存的數據。 這些操做寫在這裏,我估計是由於此時activity的建立纔剛走完。佈局

而後是onStart

public void onStart() {
        super.onStart();
        if (this.mDialog != null) {
            this.mViewDestroyed = false;
            this.mDialog.show();
        }
    }
複製代碼

記錄mViewDestroyed 爲false , 而後展現Dialog。 能夠看出在onStart生命週期中才展現Dialog,此時頁面已經展現出來。

看看彈框頁面消失的操做

public void onStop() {
        super.onStop();
        if (this.mDialog != null) {
            this.mDialog.hide();
        }
    }
複製代碼

Fragment隱藏時把Dialog也隱藏,至關於把他兩的狀態都綁在一塊兒。

public void onDestroyView() {
        super.onDestroyView();
        if (this.mDialog != null) {
            this.mViewDestroyed = true;
            this.mDialog.dismiss();
            this.mDialog = null;
        }

    }
複製代碼

mViewDestroyed = true, 記錄當前頁面已經關閉,此時dialog也跟着dismiss,而且this.mDialog = null;釋放掉內存(GC事後再不到這個引用,會來釋放掉)。這裏雖然沒什麼難理解的,可是這4行代碼寫得很是好,值得學習。

最後

public void onDetach() {
        super.onDetach();
        if (!this.mShownByMe && !this.mDismissed) {
            this.mDismissed = true;
        }

    }
複製代碼

這裏和dismissInternal方法我感受是有一種是作了多線程的感受,因此加了雙向判斷,看起來感受有點繞。dismissInternal方法是Dialog關閉時調用的。

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();
                }
            }

        }
    }
複製代碼

若是生命週期onDetach先執行,mShownByMe 仍是爲true,因此onDetach中的判斷不會走,以後還會走dismissInternal。若是dismissInternal先執行,mDismissed爲false,走判斷裏的方法, mDismissed = true his.mShownByMe = false

if (this.mDialog != null) {
       this.mDialog.dismiss();
}
複製代碼

表示沒走onDestroyView方法,因此這裏再走一次this.mDialog.dismiss(); mViewDestroyed = true mBackStackId 回退狀態,通常流程會等於-1,這時讓外層的FragmentManager移除當前Fragment。

我以爲這裏就是處理一個多線程的結果,調用的順序能夠是如下幾種狀況 (1)onDestroyView - > onDetach -> dismissInternal (2)onDestroyView - > dismissInternal-> onDetach (3)dismissInternal- > onDestroyView -> onDetach 因此能夠看出,最主要的方法是dismissInternal,它必定會調用,哪怕是在Fragment銷燬以後。因此這裏我有個問題:fragment銷燬了,那this就有可能被釋放爲空吧,那 ft.remove(this);這個操做不是有可能報空指針嗎?這個要看FragmentManager的源碼以後才知道,也許它在裏面有判空操做。 他的這個多線程的邏輯應該是挺穩定的,就是看着會很繞,若是先調生命週期再調dismissInternal基本是沒問題,若是先調dismissInternal再調onDestroyView 的話,onDestroyView裏面的this.mDialog.dismiss();仍是會走一遍,只不過mViewDestroyed已經爲true,不會再走dismissInternal裏面的邏輯。 並且Dialog內部的dismiss方法裏的邏輯也有判斷,防止屢次調用。 因此能夠看出java的多線程是一個很麻煩的傢伙,爲了保證調用順序沒問題,須要加一大堆判斷,並且久了可能連本身也看得懵,很差意思扯遠了。

最後還有一個方法沒講到—— onCreateDialog 他是在調用fragment的onGetLayoutInflater方法時調用的,onGetLayoutInflater方法是在fragment中的getLayoutInflater()方法調用後調用的,而這個getLayoutInflater()我暫時也找不到在哪裏調用,可是咱們能夠經過打印的方式來判斷onCreateDialog再哪一個生命週期之間調用。

能夠看到,是在調onCreateView以前調用的。

因此說DialogFragment裏面的Dialog在onCreate以後建立,在onStart中展現,在onDestroyView中關閉。 也能看出,Fragment中其實並無作什麼複雜的邏輯操做,都是在處理生命週期、保存數據這些操做,能夠看出這很符合谷歌說的建議用DialogFragment代替Dialog的概念,確實是加了一層用於管理的Fragment。因此最核心的功能仍是Dialog的功能,最核心的代碼仍是Dialog的代碼。

二.Dialog源碼簡單分析

相比DialogFragment,這裏我不會像上面同樣那麼詳細的分析,我只會分析某些方法。 從DialogFragment能夠在知道在onCreate以後建立Dialog

@NonNull
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
        return new Dialog(this.getActivity(), this.getTheme());
    }
複製代碼

咱們進去看看構造方法作了什麼

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        if (createContextThemeWrapper) {
            if (themeResId == ResourceId.ID_NULL) {
                final TypedValue outValue = new TypedValue();
                context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
                themeResId = outValue.resourceId;
            }
            mContext = new ContextThemeWrapper(context, themeResId);
        } else {
            mContext = context;
        }

        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

        final Window w = new PhoneWindow(mContext);
        mWindow = w;
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        w.setOnWindowSwipeDismissedCallback(() -> {
            if (mCancelable) {
                cancel();
            }
        });
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);

        mListenersHandler = new ListenersHandler(this);
    }
複製代碼

首先若是判斷是否在建立時有傳theme

這就是咱們以前說的傳0表示沒樣式。 能夠額外說說這個默認的樣式(這章暫時用不上),能夠在themes.xml中找到

<!-- Dialog attributes -->
        <item name="dialogTheme">@style/Theme.Dialog</item>
        <item name="dialogTitleIconsDecorLayout">@layout/dialog_title_icons</item>
        <item name="dialogCustomTitleDecorLayout">@layout/dialog_custom_title</item>
        <item name="dialogTitleDecorLayout">@layout/dialog_title</item>
        <item name="dialogPreferredPadding">@dimen/dialog_padding</item>
        <item name="dialogCornerRadius">0dp</item>
複製代碼

mContext = new ContextThemeWrapper(context, themeResId); 就是把themeResId給保存到ContextThemeWrapper裏面。 以後建立一個Window ,final Window w = new PhoneWindow(mContext);而且設置一些監聽的事件,而且設置Gravity居中(因此默認的Dialog都是居中顯示)。 其實能夠模仿他們這裏的獲取windowManger的方法

mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
複製代碼

這樣就建立好了Dialog,Dialog就是一個window。

而後設置佈局給Dialog

能夠看出在onActivityView中把佈局設置給Dialog,跳進去看源碼

public void setContentView(@NonNull View view, @Nullable ViewGroup.LayoutParams params) {
        mWindow.setContentView(view, params);
    }
複製代碼

也就是給這個window設置View

以後看看Dialog的展現

在onStart中展現Dialog,跳進去看源碼

public void show() {
        if (mShowing) {
            if (mDecor != null) {
                if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
                    mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
                }
                mDecor.setVisibility(View.VISIBLE);
            }
            return;
        }

        mCanceled = false;

        if (!mCreated) {
            dispatchOnCreate(null);
        } else {
            // Fill the DecorView in on any configuration changes that
            // may have occured while it was removed from the WindowManager.
            final Configuration config = mContext.getResources().getConfiguration();
            mWindow.getDecorView().dispatchConfigurationChanged(config);
        }

        onStart();
        mDecor = mWindow.getDecorView();

        if (mActionBar == null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
            final ApplicationInfo info = mContext.getApplicationInfo();
            mWindow.setDefaultIcon(info.icon);
            mWindow.setDefaultLogo(info.logo);
            mActionBar = new WindowDecorActionBar(this);
        }

        WindowManager.LayoutParams l = mWindow.getAttributes();
        boolean restoreSoftInputMode = false;
        if ((l.softInputMode
                & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
            l.softInputMode |=
                    WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
            restoreSoftInputMode = true;
        }

        mWindowManager.addView(mDecor, l);
        if (restoreSoftInputMode) {
            l.softInputMode &=
                    ~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
        }

        mShowing = true;

        sendShowMessage();
    }
複製代碼

mWindow.getDecorView()這個要詳細說又要扯到window,一直扯其它的估計都講不完,因此這裏先不講window。先把這行代碼理解成獲取window頂層的view。 而後設置ActionBar,通常咱們都沒有的。 以後這行就很熟悉,就是設置window的屬性

WindowManager.LayoutParams l = mWindow.getAttributes();
複製代碼

以後就是處理軟鍵盤的操做。注意,這個show方法中最關鍵的方法就是顯示window的頁面,也就是這句代碼(由於window的相關內容不打算在這章講),因此先了解。

mWindowManager.addView(mDecor, l);
複製代碼

最後在記錄當前狀態爲展現。

咱們再來額外先看看隱藏的方法
public void hide() {
        if (mDecor != null) {
            mDecor.setVisibility(View.GONE);
        }
    }
複製代碼

能夠看出並無關閉window,只是對view作setVisibility隱藏操做。

最後看看關閉彈框

在onDestroyView中關閉Dialog,跳進去看看源碼

void dismissDialog() {
        if (mDecor == null || !mShowing) {
            return;
        }

        if (mWindow.isDestroyed()) {
            Log.e(TAG, "Tried to dismissDialog() but the Dialog's window was already destroyed!");
            return;
        }

        try {
            mWindowManager.removeViewImmediate(mDecor);
        } finally {
            if (mActionMode != null) {
                mActionMode.finish();
            }
            mDecor = null;
            mWindow.closeAllPanels();
            onStop();
            mShowing = false;

            sendDismissMessage();
        }
    }
複製代碼

也很簡單,就是移除窗口,而後再改變狀態。

mWindowManager.removeViewImmediate(mDecor);
複製代碼

可能有人會問,咦,那爲何沒有看到在哪裏設置樣式。 若是咱們傳的是資源文件來設置樣式的話,資源文件會傳給context,context會傳給window,樣式的設置就是在window內部設置的。若是咱們動態設置樣式的話,通常都寫

getDialog().getWindow().XXXXXXX
複製代碼

這樣設置也是傳給window來設置。

三. 總結

從源碼咱們能夠看出,DialogFragment實質上仍是操做Dialog,而Dialog實質上是操做Window。因此咱們是否是得出一個結論,若是想測試某個屬性對Dialog有什麼影響,基本上能夠直接測這條屬性對Window有什麼影響。 以後我總結的Dialog的一些屬性的分析,就能夠寫到Window相關的地方,關鍵的仍是window,可是window的源碼就沒Dialog的這麼簡單了,這個以後再講。

相關文章
相關標籤/搜索