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的这么简单了,这个以后再讲。

相关文章
相关标签/搜索