前言:这段时候有点忙,因为在赶项目,说忙也都是在敷衍,时间挤挤还是有的,从开始写博客的那一刻起就应该一直坚持下来,不要三天打鱼两天晒网,上次写过一个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,接下来还会写一篇关于这个工具类的博客,未完待续,(^__^) 嘻嘻……