首页 > 代码库 > Android为啥推荐用DialogFragment创建Dialog?

Android为啥推荐用DialogFragment创建Dialog?

前言:这段时候有点忙,因为在赶项目,说忙也都是在敷衍,时间挤挤还是有的,从开始写博客的那一刻起就应该一直坚持下来,不要三天打鱼两天晒网,上次写过一个Android进阶之(dialog详解一),今天继续上次的内容探究一下,从Android3.0后Android引入的DailogFragment,我们一起从源码的角度去看看为啥谷歌推荐这样创建Dialog.
DialogFragment对话框出现的意义

为什么android系统有AlertDialog,PopupWindow对话框,基本满足客户需求,为啥还要跑出一个DialogFragment对话框呢?
这就要从DialogFragment的优点说起了:

有和Fragment基本一致的生命周期,因此便于Activity更好的控制管理DialogFragment。 随屏幕旋转(横竖屏幕切换)DialogFragment对话框随之自动调整对话框大小。而AlertDialog和PopupWindow随屏幕切换而消失。 DialogFragment的出现解决 横竖屏幕切换Dialog消失的问题。

既然是探究性的话题,我们就从DialogFragment源码探究一下。
在此次之前先贴一张Fragment的生命周期图:
技术分享

DialogFragment的全部源码我就不贴了,要研究的童鞋就多按按Ctrl+左键吧(^__^) 嘻嘻……我们按照Fragment的生命周期一个一个往下看,揭开它的神秘面纱。
首先看看onAttach()方法

 @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (!mShownByMe) {
            // 如果不是通过调用DialogFragment自己的show方法弹出Dialog的话
            // 标志着这个Dialog不再被消失
            mDismissed = false;
        }
    }

这个方法也没什么重要代码。
接下来看看onCreate()方法

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

当Activity生命周期发生变换的时候,也就是比如切换横竖屏的时候,对Dialog的一些属性进行保存,当再次创建Fragment的时候回到上一次Dialog的状态,既然有取的过程,那必定有存的过程。

 @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        if (mDialog != null) {
            Bundle dialogState = mDialog.onSaveInstanceState();
            if (dialogState != null) {
                outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState);
            }
        }
        if (mStyle != STYLE_NORMAL) {
            outState.putInt(SAVED_STYLE, mStyle);
        }
        if (mTheme != 0) {
            outState.putInt(SAVED_THEME, mTheme);
        }
        if (!mCancelable) {
            outState.putBoolean(SAVED_CANCELABLE, mCancelable);
        }
        if (!mShowsDialog) {
            outState.putBoolean(SAVED_SHOWS_DIALOG, mShowsDialog);
        }
        if (mBackStackId != -1) {
            outState.putInt(SAVED_BACK_STACK_ID, mBackStackId);
        }
    }

紧接着我们看到这么一个方法:

 /** @hide */
    @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);
    }

这个方法虽然隐藏了,但是我们看到,在方法里面new了一个Dialog,onCreateDialog,所以我们知道,其实DialogFragment里面也是悄悄的创建了一个Dialog,创建了一个Dialog,那系统怎么知道我们创建的Dialog的样式呢?总要一些Dialog的初始化吧?

带着疑问,接着我们看看onActivityCreated这个方法:

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

我们看到有这么一段代码:

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

看到这个我们很熟悉,就是给Dialog设置了一个布局文件,那么这个布局又是哪来的呢?View view = getView();,这里的getView获取到的值,正是我们在onCreateView中return的那个view。那么我们在onCreateView中返回了什么,我们Dialog就会显示啥样。

看到这里我们一定有点思路了,如果要用DialogFragment来弹出一个Dialog,有两种方法。
一:重写createDialog方法,给一个自己的Dialog

@NonNull
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        return new Dialog(getActivity(), getTheme());
    }

二:重写Fragment的onCreateView方法,返回一个自定义个布局

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

好了,现在Dialog创建好了,也初始化过了,那么怎么显示Dialog呢?
机智的你肯定有思路了,
一:调用DialogFragment的getDialog方法然后show

 public Dialog getDialog() {
        return mDialog;
    }

二:调用DialogFragment的show方法

public void show(FragmentManager manager, String tag)

public int show(FragmentTransaction transaction, String tag) 

但是这只是把一个没有UI的Fragment添加到了Activity的Fragment栈中,Dialog又是哪弹出来的呢?
我们接着Fragment的生命周期看看onStart方法,这个方法也就是当Activity可见的时候调用。

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

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

我们很清晰的看到了mDialog.show();此时Dialog对用户可见。
再延伸一下,我们可以看到这里有一个 if (mDialog != null)的判断,那么什么时候mDialog==null?我们看看它在哪创建的:

/** @hide */
    @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);
    }

也就是当mShowsDialog为false的时候,我们的dialog就是null了。
我们再onCreate方法中看到这么一行代码:

mShowsDialog = mContainerId == 0;

那么这个mContainerId 又是什么呢?

// When a fragment is being dynamically added to the view hierarchy, this
    // is the identifier of the parent container it is being added to.

英语不是很好,大致翻译一下,此id就是你需要将Fragment添加的那个容器,也就是跟我们平时使用的普通的Fragment一样,当需要把一个Fragment添加到Activity的时候,调用

  FragmentTransaction ft = manager.beginTransaction();
        ft.add(R.id.container, tag);

到这,我们终于把DialogFragment的源码跟着Fragment的生命周期跑了一遍,总结一下:
DialogFragment也就是在内部new了一个Dialog然后显示在Activity上,但是有了Fragment的附属后,我们有了一套完整的生命周期了,Dialog依附Fragment从而当Activty生命周期发生变换的时候,会重新把Fragment添加进Activity中,从而不会使Dialog消失。

说了这么多原理性的东西,我们都有点疲惫了,接下来让我们来实战一下~~~~

为了展示效果,我们还是模仿一下微信的Dialog吧,先看看效果
技术分享
普通的创建Dialog的方法,我在Android进阶之(dialog详解一),已经有实现了,如果有需要的朋友可以直接去拖代码,(^__^) 嘻嘻……

方式一:重写DialogFragment的onCreateView

wechat_dialog.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/parentPanel"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >
    <RelativeLayout
        android:id="@+id/body"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:padding="10dp" >
        <ProgressBar
            android:id="@+id/progress"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_marginRight="12dip"
            android:indeterminate="false"
            android:indeterminateDrawable="@drawable/shape_ring"
             />
        <TextView
            android:id="@+id/customFrameMsg"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_toRightOf="@id/progress"
            android:singleLine="true"
            android:text="正在登录..."
            android:textColor="#ffffff"
            android:textSize="14.5sp" />
    </RelativeLayout>

</RelativeLayout>

布局很简单,我们定义一个Fragment叫WechatFragment

package com.cisetech.dialogdemo.dialog;

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.cisetech.dialogdemo.R;

/**
 * author:yinqingy
 * date:2016-11-01 22:20
 * blog:http://blog.csdn.net/vv_bug
 * desc:
 */

public class WechatFragment extends DialogFragment {
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.wechat_dialog,null);
    }
}

我们创建Fragment然后调用show代码:

public void showWechatDialog(){
        WechatFragment wechatDialog=new WechatFragment();

        wechatDialog.show(getSupportFragmentManager(),"wechatDialog");
    }

然后我们运行看看效果:

技术分享

好吧,Dialog是显示出来了,出来的是这吊样,跟我们想要的结果还很远啊,
1、去掉Dialog默认的Title位置,然后背景设成自己的透明色。

 public void showWechatDialog(){
        WechatFragment wechatDialog=new WechatFragment();
        wechatDialog.setStyle(DialogFragment.STYLE_NO_TITLE,R.style.loading_dialog);
        wechatDialog.show(getSupportFragmentManager(),"wechatDialog");
    }

Dialog的style文件:

 <!-- 自定义loading dialog -->
    <style name="loading_dialog" parent="android:style/Theme.Dialog">
        <!--提示框是否有边框-->
        <item name="android:windowFrame">@null</item>
        <!--是否需要标题-->
        <item name="android:windowNoTitle">true</item>
        <!--对话框的背景-->
        <item name="android:windowBackground">@drawable/pd_shape</item>
        <!--对话框是否悬浮-->
        <item name="android:windowIsFloating">true</item>
        <!--默认Window的content背景-->
        <item name="android:windowContentOverlay">@null</item>
        <!--dialog遮罩透明度-->
        <item name="android:backgroundDimAmount">0.5</item>
        <!--是否需要dialog遮罩-->
        <item name="android:backgroundDimEnabled">true</item>
    </style>

shape文件:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
    <corners android:radius="8dp"/>
    <solid android:color="#7f000000"/>
</shape>

然后我们再运行下代码:
技术分享

好吧,经过我们的修改我们终于离我们的目标就差一点了,就是Dialog的宽度了,我们让Dialog的宽度为屏幕宽的0.618,(^__^) 嘻嘻……
重写onStart()方法,在dialog弹出之后进行修改宽度属性

@Override
    public void onStart() {
        super.onStart();
        Dialog dialog =getDialog();//获取Dialog
        WindowManager.LayoutParams attr = dialog.getWindow().getAttributes();//获取Dialog属性
        WindowManager wm= (WindowManager) dialog.getContext().getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetric=new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(outMetric);
        attr.width= (int) (outMetric.widthPixels*0.618f);
        dialog.getWindow().setAttributes(attr);
    }

再次运行代码:
技术分享

可以看到我们已经成功达到了我们的目标,还没完。。。
我们转换屏幕看看什么效果:
技术分享

可以看到,效果是一致的。(^__^) 嘻嘻……

我们使用直接new Dialog的形式,然后切换横屏试试:
效果还是一致,但是系统报了一个错误

android.view.WindowLeaked: Activity com.cisetech.dialogdemo.MainActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView{230312c5 V.E..... R....... 0,0-791,140} that was originally added here
                                                                          at android.view.ViewRootImpl.<init>(ViewRootImpl.java:363)
                                                                          at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:265)
                                                                          at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
                                                                          at android.app.Dialog.show(Dialog.java:298)
                                                                          at com.cisetech.dialogdemo.MainActivity.onCreate(MainActivity.java:94)
                                                                          at android.app.Activity.performCreate(Activity.java:5941)
                                                                          at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1105)
                                                                          at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2256)
                                                                          at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2363)
                                                                          at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:3915)
                                                                          at android.app.ActivityThread.access$900(ActivityThread.java:147)
                                                                          at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1289)
                                                                          at android.os.Handler.dispatchMessage(Handler.java:102)
                                                                          at android.os.Looper.loop(Looper.java:135)
                                                                          at android.app.ActivityThread.main(ActivityThread.java:5235)
                                                                          at java.lang.reflect.Method.invoke(Native Method)
                                                                          at java.lang.reflect.Method.invoke(Method.java:372)
                                                                          at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:906)
                                                                          at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:701)

虽然效果一样,但是如果我们Dialog是一个输入用户名密码的Dialog,那么当切换屏幕的时候,用直接new Dialog的话会被清空掉,如果是DialogFragment的话则不会被清空。

方式二,重写onCreateDialog实现DialogFragment弹出Dialog

MainActivity.java

 private WechatFragment wechatDialog;
    public void showWechatDialog(){
        if(wechatDialog!=null&&wechatDialog.getDialog()!=null&&wechatDialog.getDialog().isShowing()){
            wechatDialog.dismiss();
            return;
        }
        wechatDialog=new WechatFragment();
       // wechatDialog.setStyle(DialogFragment.STYLE_NO_TITLE,R.style.loading_dialog);
        wechatDialog.show(getSupportFragmentManager(),"wechatDialog");
    }

WechatFragment.java:

 @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        LayoutInflater inflater = LayoutInflater.from(getActivity());
        View v = inflater.inflate(R.layout.wechat_dialog, null);// 得到加载view
        Dialog loadingDialog = new Dialog(getActivity(), R.style.loading_dialog);// 创建自定义样式dialog
        loadingDialog.setCancelable(true);// 不可以用“返回键”取消
        loadingDialog.setContentView(v);// 设置布局
        return loadingDialog;
    }

运行效果:
技术分享

和我们上面那种重写onCreateView的方式效果一样。

到此~ DialogFragment的基本内容已经完毕了,以前看到一些大牛封装的DialogUtil,接下来还会写一篇关于这个工具类的博客,未完待续,(^__^) 嘻嘻……

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    Android为啥推荐用DialogFragment创建Dialog?