DialogFragment源码解析与应用

版权声明:本文为博主原创文章,未经博主允许不得转载

前言


DialogFragment 是继承于 Fragment 的子类,因而其具备了 Fragment 的特性,如生命周期。而其内部实际控制了一个 Dialog 对象,并通过当前 DialogFragment所依附的 ActivityFragmentManager 来对其进行管理与控制,也即将其当做一个普通的 Fragment 那样来管理。

注:dialog 拥有自己的窗体 window,意味着它可以自己接收输入事件,处理事件,并且决定自己何时消失。

浅谈生命周期


DialogFragmentDialog 同生同死,因为 DialogFragment 需要监听 Dialog的显示与关闭,并做出反映。而这两个操作关联到了 DialogFragment 加入到 FragmentManager 中和从其中移除。这样一来 Dialog的创建与销毁也随着 DialogFragment 的生命周期而走了。

源码分析

public void setStyle(@DialogStyle int style, @StyleRes int theme) {
    mStyle = style;
    if (mStyle == STYLE_NO_FRAME || mStyle == STYLE_NO_INPUT) {
        mTheme = android.R.style.Theme_Panel;
    }
    if (theme != 0) {
        mTheme = theme;
    }
} 

此方法为对话框设置样式与主题。其中样式有 4 个可选,一般用得比较多的是STYLE_NO_TITLE(指定对话框无标题模式),而主题默认的话可填0,若是自定义主题,可在style文件下新增一个样式,然后继承Theme.Dialog修改你期望的主题即可。

注意: 此方法若要生效一定要在对话框未创建之前设置,否则失效。下面我们还会谈到。

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

此方法就是控制对话框的显示,调用即可。其实就是类似普通的 Fragment的显示操作逻辑,获取FragmentManager来管理当前的对话框。是不是似曾相识?

 public void dismiss() {
    dismissInternal(false);
}
void dismissInternal(boolean allowStateLoss) {
    if (mDismissed) {
        return;
    }
    mDismissed = true;
    mShownByMe = false;
    if (mDialog != null) {
        mDialog.dismiss();
        mDialog = null;
    }
    mViewDestroyed = true;
    if (mBackStackId >= 0) {
        getFragmentManager().popBackStack(mBackStackId,
                FragmentManager.POP_BACK_STACK_INCLUSIVE);
        mBackStackId = -1;
    } else {
        FragmentTransaction ft = getFragmentManager().beginTransaction();
        ft.remove(this);
        if (allowStateLoss) {
            ft.commitAllowingStateLoss();
        } else {
            ft.commit();
        }
    } 

此方法控制DialogFragment的关闭。我们看到在void dismissInternal()方法内会判断当前的Dialog对象 mDialog是否为空,否则就调用Dialogdismiss() 关闭对话框。而剩下的代码都是DialogFragment为了管理自己的后续操作。比如退栈操作,移除 Fragment

 public Dialog getDialog() {
    return mDialog;
}

此方法用于获取 DialogFragment 内的 Dialog 对象。我们之后要对对话框的尺寸改变、去除标题、设置背景、都需要获取该实例。

 public void setCancelable(boolean cancelable) {
    mCancelable = cancelable;
    if (mDialog != null) mDialog.setCancelable(cancelable);
}

望名知义,就是设置对话框是否可以取消。内部还是调用 Dialog的方法

 public void setShowsDialog(boolean showsDialog) {
    mShowsDialog = showsDialog;
}  

此方法控制当前 DialogFragment是否作为对话框而存在,否则视作普通的 Fragment来使用。而如果你把它当前普通的 Fragment加入到关联的 Activity中,那么mShowsDialog 会被初始化为 true

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

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

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mShowsDialog = mContainerId == 0;

    if (savedInstanceState != null) {
        mStyle = savedInstanceState.getInt(SAVED_STYLE, STYLE_NORMAL);
        mTheme = savedInstanceState.getInt(SAVED_THEME, 0);
        mCancelable = savedInstanceState.getBoolean(SAVED_CANCELABLE, true);
        mShowsDialog = savedInstanceState.getBoolean(SAVED_SHOWS_DIALOG, mShowsDialog);
        mBackStackId = savedInstanceState.getInt(SAVED_BACK_STACK_ID, -1);
    }
}  

以上的三个方法都是 重写 Fragment 中的生命周期方法。前两个主要是设置相应的标志位。
而在 onCreate() 中主要是从保存的实例状态中获取相应信息(之前在 onSaveInstanceState() 中设置的)。

/** @hide */
@RestrictTo(LIBRARY_GROUP)
@Override
public LayoutInflater getLayoutInflater(Bundle savedInstanceState) {
    if (!mShowsDialog) {
        return super.getLayoutInflater(savedInstanceState);
    }

    mDialog = onCreateDialog(savedInstanceState);

    if (mDialog != null) {
        setupDialog(mDialog, mStyle);

        return (LayoutInflater) mDialog.getContext().getSystemService(
                Context.LAYOUT_INFLATER_SERVICE);
    }
    return (LayoutInflater) mHost.getContext().getSystemService(
            Context.LAYOUT_INFLATER_SERVICE);
}

/** @hide */
@RestrictTo(LIBRARY_GROUP)
public void setupDialog(Dialog dialog, int style) {
    switch (style) {
        case STYLE_NO_INPUT:
            dialog.getWindow().addFlags(
                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
                            WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
            // fall through...
        case STYLE_NO_FRAME:
        case STYLE_NO_TITLE:
            dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
    }
}  
public Dialog onCreateDialog(Bundle savedInstanceState) {
    return new Dialog(getActivity(), getTheme());
}

最上两个方法是被隐藏的。getLayoutInflater() 中首先判断是否显示为一个对话框,若是才来到下面的逻辑。在这里我们发现,方法内调用了onCreateDialog()来获得一个对话框实例。如果实例存在,那么才开始配置对话框(注意:我们发现此处获取前述的 setStyle()传进来的值,因而该方法的调用必须早于此,也就是说你至少可以在 onCreateDialog()中进行设置),此时,方法返回与对话框上下文关联的布局加载器。
接下来我们看setupDialog()。方法内根据存入的样式,对对话框实例进行一系列的设置,比如禁止获取焦点、不可触摸、设置无标题。
另外,在 onCreateDialog() 返回了一个新的对话框实例。在此,我们注意到,如果你想实现一些Dialog子类的对话框,比如 AlertDialog,那么你就可以重写该方法,返回你要创建的Dialog 子类实例。

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);

    if (!mShowsDialog) {
        return;
    }

    View view = getView();
    if (view != null) {
        if (view.getParent() != null) {
            throw new IllegalStateException(
                    "DialogFragment can not be attached to a container view");
        }
        mDialog.setContentView(view);
    }
    final Activity activity = getActivity();
    if (activity != null) {
        mDialog.setOwnerActivity(activity);
    }
    mDialog.setCancelable(mCancelable);
    mDialog.setOnCancelListener(this);
    mDialog.setOnDismissListener(this);
    if (savedInstanceState != null) {
        Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG);
        if (dialogState != null) {
            mDialog.onRestoreInstanceState(dialogState);
        }
    }
}  

Activity创建完成后回调。若mShowsDialogfalse,那么以下逻辑不再执行。
否则获取当前 DialogFragment 的根布局实例 view,并将其作为对话框的内容,加入其中。接下来把对话框与 Activity 进行绑定。接下来就是对话框的配置,包括注册取消监听,关闭监听。最后保存相关状态到 savedInstanceState,以便重建时利用。

 @Override
public void onStart() {
    super.onStart();

    if (mDialog != null) {
        mViewDestroyed = false;
        mDialog.show();
    }
}  

此方法很重要。对话框真正显示就在此后。内容调用了 mDialogshow()方法,将对话框显示出来(依附于 Fragment)

     @Nullable
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
        @Nullable Bundle savedInstanceState) {
    return null;
}

此方法为 Fragment 中的方法,为空实现,它交给子类去加载一个 contentView,再由Fragment 来处理。而我们对话框的内容布局就是在此处进行加载,以便返回一个对话框的布局给 Fragment 去处理。

注意:onCreateDialog()onCreateView() 先一步执行。因为在 Fragment中调用onCreateView() 时需要拿到一个布局加载器LayoutInflater,而DialogFragmet中是通过上述的一个方法—getLayoutInflater()来返回一个与对话框有关的上下文布局加载器,onCreateDialog()就在其中被调用,因而我们很容易就可以知道两者的执行次序。

几类常见应用

普通对话框

public class CommonDialogFragment extends DialogFragment {

private static final String TAG = "CommonDialogFragment";


@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
     Dialog dialog = getDialog();
    DisplayMetrics dm = DensityUtil.getDisplayMetrics(getActivity());
    int width = dm.widthPixels;
    if (dialog != null) {
        dialog.getWindow().setLayout(width * 2 / 3, width / 2);
        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));//去除多余的padding
    }
    dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
    View view = inflater.inflate(R.layout.time, container, false);
    ListView mListView = (ListView) view.findViewById(R.id.listview);
    mListView.setAdapter(new MyAdapter(getActivity()));
    return view;
    }
}

这类很简单,一般只要重写 onCreateView(),和Fragment中的做法一样。只是需要额外配置对话框的一些设置,我们下面再统一叙述。

警告对话框

public class MyAlerDialog extends DialogFragment {

private static final String TAG = "RoundCornerDialog";

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    return new AlertDialog.Builder(getActivity()).setMessage("我是对话框").setTitle("警告").setIcon(R.mipmap.ic_launcher).create();
}

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
    Dialog dialog = getDialog();
    DisplayMetrics dm = DensityUtil.getDisplayMetrics(getActivity());
    int width = dm.widthPixels;
    if (dialog != null) {
        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));//去除多余的padding
        dialog.getWindow().setLayout(width * 2 / 3, width / 2);
    }
    return super.onCreateView(inflater, container, savedInstanceState);
}

}

圆角对话框

圆角对话框与上述两张对话框创建流程无异,只需对布局额外加上一层drawable绘制的圆角背景层即可

注意: 若加了圆角背景层仍没有圆角化,那么你需要对圆角背景层再 加上padding属性即可解决。

对话框的配置

去标题化

  1. 调用setStyle()传入STYLE_NO_TITLE,必须在Dialog创建之前调用方可生效。
  2. 定义自己的主题,设置 windowNoTitle属性为true

背景透明化

getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)。

我们一般都会加上,没有设置有可能出现多余的padding,它可让整个Window透明化。

设置Dialog的尺寸

由于 Dialog 属于 Window 级别,属于独立窗体,因而要改变 Dialog 的尺寸实际上就上改变窗体 Window 的尺寸,而如果你对布局修改尺寸是毫无效果的。
我们通过getDialog().getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)来设置对话框具体宽高或全屏就够了。

设置对话框方位

实际还是控制window这个对象,调用它的方法setGravity()即可。

注意: 但凡涉及到获取对话框所在的window对象时,都必须在对话框已经创建了之后调用。

配置style样式定制主题

我们查看源码可知道对话框的默认主题基础自Theme.Dialog,那么我们便可以继承整个主题,修改默认主题中的属性值来达到我们想要的效果,下面贴出对话框默认的主题。

    <style name="Theme.Dialog">
    <!--配置window有无边框-->
    <item name="windowFrame">@null</item>
    <!--配置window标题样式-->
    <item name="windowTitleStyle">@style/DialogWindowTitle</item>
    <!--配置window背景层-->常用
    <item name="windowBackground">@drawable/panel_background</item>
    <!--配置window是否浮在上面-->
    <item name="windowIsFloating">true</item>
    <!--配置window是否浮在上面-->
    <!--配置window有无标题 --> 常用
    <item name="windowNoTitle">true</item>
    <!--配置window进入退出动画 --> 常用
    <item name="windowAnimationStyle">@style/Animation.Dialog</item>
    <!--配置window外部是否可触摸-->
    <item name="windowCloseOnTouchOutside">@bool/config_closeDialogWhenTouchOutside</item>
    ....此处省略部分属性
    </style>

总结

首先,介绍了使用DialogFragment 的缘由以及好处,再来谈到了它的生命周期,这也是之所以使用这种方式来呈现对话框的关键所在。再接下来,我们从源码的角度,详细剖析了各个方法的作用、联系,以及调用的时机以及注意事项。再来介绍了DialogFragment的几种常见的应用,也可以说是常用的代码模版了。最后,介绍了配置对话框的一些常用的手段以及注意事项。

感谢

感谢您看到这里,期望留下您的印迹。

-------------本文结束感谢您的阅读-------------