详细解读DialogFragment
相信看这篇文章的人都应该知道android中的Dialog了吧,如果对于Dialog还不是很了解可以看我之前的一篇详解文章:
Dialog详解:http://www.cnblogs.com/tianzhijiexian/p/3867731.html
随着Fragment这个类的引入,Google官方推荐大家使用DialogFragment来代替传统的Dialog,那么是不是说我们之前学习的Dialog知识都没有用处了呢?非也,新的fragment是来方便大家更好的管理和重用Dialog,之前的知识其实都是可以拿来就用的,仅仅需要少许的改变。
一、Dialog和DialogFragment的区别和优劣
新来的DialogFragment让dialog也变成了碎片,相比之前来说应该做了很多优化和处理,对于程序员来看对话框现在更加具体了,就是一个activity上的一个fragment,我们也可以用fragment的知识来管理对话框了。
我们看看之前是怎么运用对话框对象的
AlertDialog dialog = new AlertDialog.Builder(this) .setTitle("Dialog") .setMessage("thisis a dialog") .show();
如果这个时候屏幕方向发生变化,就会导致Activity重建,然后之前显示的对话框就不见了。查看log可以发现这个错误:
04-1917:30:06.999: E/WindowManager(14495): Activitycom.example.androidtest.MainActivity has leaked windowcom.android.internal.policy.impl.PhoneWindow$DecorView{42ca3c18 V.E.....R....... 0,0-1026,414} that was originally added here
当然我们也可以无视这个错误,因为程序不会因此崩溃(看来android本身就已经预料到这种情况了)。
如果我们想要在旋转屏幕的时候也能保证这个对话框显示就需要做一定的处理了,在activity要销毁前设立一个标志,看这时对话框是否是显示状态,如果是那么activity在下次建立时直接显示对话框。
在onSaveInstanceState中
@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (dialog != null && dialog.isShowing()) { outState.putBoolean("DIALOG_SHOWN", true); } }
在onCreat中
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState != null) { boolean isShown = savedInstanceState.getBoolean("DIALOG_SHOWN"); if (isShown) { AlertDialog dialog = new AlertDialog.Builder(this).setTitle("Dialog") .setMessage("thisis a dialog").show(); } } …… }
使用DialogFragment来管理对话框就不会有这种问题了,代码也少了很多的逻辑处理。当你旋转屏幕的时候,fragmentManager会自定管理DialogFragment的生命周期,如果当前已经显示在屏幕上了,那么旋转屏幕后夜会自动显示,下面就是在屏幕旋转时的log输出。
4-1917:45:41.289: D/==========(16156): MyDialogFragment : onAttach
04-1917:45:41.299: D/==========(16156): MyDialogFragment : onCreate
04-1917:45:41.299: D/==========(16156): MyDialogFragment : onCreateView
04-1917:45:41.309: D/==========(16156): MyDialogFragment : onStart
04-1917:45:50.619: D/==========(16156): MyDialogFragment : onStop
04-1917:45:50.619: D/==========(16156): third activity on destroy
04-1917:45:50.619:D/==========(16156): MyDialogFragment : onDestroyView
04-1917:45:50.619: D/==========(16156): MyDialogFragment : onDetach
04-1917:45:50.639: D/==========(16156): MyDialogFragment : onAttach
04-1917:45:50.639: D/==========(16156): MyDialogFragment : onCreate
04-1917:45:50.659: D/==========(16156): MyDialogFragment : onCreateView
04-1917:45:50.659: D/==========(16156): MyDialogFragment : onStart
Ok,当然你可以不以为然,你说我的应用就是竖着用的,旋转屏幕毕竟是小概率事件,谁会开着对话框旋转来旋转去啊。那么相信下面的好处你一定不能否定吧。
我们之前用Dialog的时候,在activity中必须要建立这个对象,而且一般我们都是需要给它放监听器的,比如下面的代码:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.guid_main);new AlertDialog.Builder(GuideActivity.this).setTitle("用户申明") .setMessage(getResources().getString(R.string.statement)) .setPositiveButton("我同意", new positiveListener()) .setNegativeButton("不同意", new negativeListener()) .setCancelable(false) .show(); }private class positiveListener implements DialogInterface.OnClickListener { @Override public void onClick(DialogInterface dialog, int which) { prefs.setIsFirstTime(false); } } private class negativeListener implements DialogInterface.OnClickListener { @Override public void onClick(DialogInterface dialog, int which) { Util.virtualHome(GuideActivity.this); } }
你会发现这么长的代码很破坏activity中的逻辑性,有木有!!!在activity中我们处理的各种控件的显示和逻辑,但对于dialog这种不属于activity并且建立和处理逻辑都自成一体的东西,我们为什么要在activity中建立呢?而且为了方便重用,我们在实际过程中基本都会建立一个dialog的工具类来做处理,所以为什么不用DialogFragment来实现呢?如果通过它来实现,我们就能很方便的进行管理对话框。
此外,当旋转屏幕和按下后退键时可以更好的管理其声明周期,它和Fragment有着基本一致的声明周期。且DialogFragment也允许开发者把Dialog作为内嵌的组件进行重用,类似Fragment(可以在大屏幕和小屏幕显示出不同的效果)。有可能我们在大屏幕上就不需要弹出一个对话框了,直接内嵌在activity界面中显示即可。这点也很赞!
二、DialogFragment的最简单用法
使用DialogFragment很简单,甚至比用Fragment还简单,因为在api中已经实现了fragment切换对象了。
1.建立一个fragment对象
package com.kale.dialogfragmenttest; import android.app.DialogFragment; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; public class MyDialogFragment extends DialogFragment{ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { System.out.println("tag = "+getTag()); // tag which is from acitivity which started this fragment return inflater.inflate(R.layout.dialog, null); } }
我们建立了一个fragment,让他继承了DialogFragment,在onCreatView中通过布局文件建立了一个view,这和fragment完全一致。
布局文件如下:
2.在activity中启用这个dialog
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); new DialogFragmentTest() .show(getFragmentManager(), "dialog_fragment"); }
很像Dialog吧,也是支持链式编程的。这里面的参数:① 一个fragmentManager,在低版本中用getSupport来获取;② 一个tag(String)通过这个tag可以告诉fragment是谁启动了它,当然这仅仅是这个tag的一种使用方式啦。在fragment中可以通过getTag()方法来获取这个tag
这里多说一句,在一年前我还一直说要兼容要兼容,不兼容的demo是很不负责任的,但是现在来看,低版本的用户真的很少很少了,而且这些低版本的用户已经不能是我们的主流用户了,所以在2014年末,我可以负责任的说,可以不用兼容2.x的系统了。我之前写过很多兼容的文章,actionbar啊,对话框的兼容啊,但现在都变得无所谓了,其实任何事物的发展都是如此。很多之前很重要的技术,在新的发展中已经慢慢变得无足轻重了,但我们之前为之付出的东西却不是无价值的。一个原因是为自己之前的工作找到价值,一种是在那段时光中我们慢慢体会到了很多东西,这些东西就是我们的阅历也是一种谈资。
好,闲话少叙,下面是运行效果:
有人会说,上面的那个空白的title好丑,我想去掉。当然可以,这就是fragment的好处,用这个方法:
getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);
public class MyDialogFragment extends DialogFragment{ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { System.out.println("tag = "+getTag()); getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE); return inflater.inflate(R.layout.dialog, null); } }
现在它变成了这个样子:
所以你可以看到,任何改变都是要付出代价的,如果你还是像之前一样用match_parent来制定控件的宽度,那么就是这种结果。可以说那个title栏就是一个房梁,支撑着对话框的宽度,没了它就只能自适应了。解决办法就是自定义控件的宽度,写个几百dp啥的,没任何技术难度。
注意:
如果你的DialogFragment是Activity的内部类,必须将DialogFragment定义为静态的。否则会报错!!!
public static class DialogFragmentTest extends DialogFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // TODO 自动生成的方法存根 return inflater.inflate(R.layout.dialog, null); } }
三、DialogFragment启动、终止过程分析
之前说了,我们没有像fragment那样建立一个fragment加载对象进行fragment的加载,也没有commit,但却能使用dialogFragment对象,这是为什么呢?
我们先来回顾下fragment是怎么使用的。
① 建立FragmentManager对象,用来管理fragment
② 建立fragmentTransaction对象,用来添加和fragment
③ 提交fragment切换(commit)
FragmentManager fragmentManager = getSupportFragmentManager(); FragmentTransaction ft = fragmentManager.beginTransaction(); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.add(R.id.container_fragment, new MyFragment()) .commit();
替换fragment的方法
getSupportFragmentManager().beginTransaction(). replace(R.id.container_fragment02, fragment) .addToBackStack(null) .commit();
现在,我们通过源码来分析下DialogFragment的启动方式
我们在使用它的时候没有去用fragmentTransaction对象,也没有执行add,也没有执行commit,仅仅提供了一个fragmentManager,那么它是怎么被添加的呢?我们知道这个对话框是用show方法显示的,那么就来看看这个方法吧。
DialogFragment源码:
1. show()
public void show(FragmentManager manager, String tag){ mDismissed = false; mShownByMe = true; FragmentTransaction ft = manager.beginTransaction(); // creat a fragmentTransaction ft.add(this, tag); // add fragment with tag ft.commit(); }
真相大白,api自动给你实现了一个fragment切换的对象,而且在show的时候就已经add了fragment,所以没有任何问题~
在add方法中没有提供容器的id,所以表示是加载到当前activity中的,在添加后也的确调用了commit方法
2. show()的另一种形式
public int show(FragmentTransaction transaction, String tag) { mDismissed = false; mShownByMe = true; transaction.add(this, tag); mViewDestroyed = false; mBackStackId = transaction.commit(); return mBackStackId; }
上面的show方法传入的是一个fragmentTransaction对象,这个也很容易理解。我们之前传入fragmentManager对象的目的就是生成这个fragmentTransaction对象,这回我们可以在传入一个已经配置好的fragmentTransaction对象,大大增加了可定制性。所以api的制订也是大神们心血的结晶啊。
3.dimiss()
源码:
** * Dismiss the fragment and its dialog. If the fragment was added to the * back stack, all back stack state up to and including this entry will * be popped. Otherwise, a new transaction will be committed to remove * the fragment. */ 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关闭的时候会检查堆栈里面有没有其他的对象,如果有就pop出来,如果没有就直接remove和commit。也就是说:如果back stack堆栈有该Dialog,将其pop出来,否则ft.remove(this); ft.commit();。估计pop的操作也包含ft.remove()和ft.commit()。调用dismiss()会触发onDismiss()回调函数。
跟踪状态,如下:
四、通过onCreateView()来建立对话框布局
上面的例子中我们已经在onCreateView()建立的对话框布局,这时fragment中建立布局的传统写法,很适合用于自定义的对话框,我们可以修改任何的东西,包括对话框的style。上面的例子中我们已经干掉了对话框上面title的区域,而我们也没发现可以设置标题的方法,感觉上面那个标题栏就是个标题党,毫无意义(之后会说到这块区域的用处)。
我们在onCreat中可以设置对话框的风格和各种属性,但是千万别设置关于view的东西,因为这时候对话框还没建立呢,有关于view的东西在onCreatView中去设置吧,这里我简单设置了一个button的点击事件——关闭对话框
public class MyDialogFragment extends DialogFragment{ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //如果setCancelable()中参数为true,若点击dialog覆盖不到的activity的空白或者按返回键, //则进行cancel,状态检测依次onCancel()和onDismiss()。如参数为false,则按空白处或返回键无反应。缺省为true setCancelable(true); //可以设置dialog的显示风格 //setStyle(style,theme); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { //getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE); View rootView = inflater.inflate(R.layout.dialog, null); Button btn = (Button)rootView.findViewById(R.id.button); btn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO 自动生成的方法存根 dismiss(); } }); return rootView; } }
补充:实现信息保存
在activity横竖屏切换的时候,dialog现在可以自动重建了,如果你在editText中输入了信息,在重建的时候会不会保留之前的呢?在4.2和4.4中对话框人性化的自定保存了之前输入的内容,我们无须手动处理。但如果你测试的手机被奇葩的定制了,那就乖乖的保存数据吧。
public class MyDialogFragment extends DialogFragment { EditText editText; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.dialog, null); editText = (EditText) rootView.findViewById(R.id.editText); if (savedInstanceState != null) { CharSequence text = savedInstanceState.getCharSequence("input data"); editText.setText(text == null ? "" : text); } return rootView; } @Override public void onSaveInstanceState(Bundle outState) { outState.putCharSequence("input data", editText.getText()); super.onSaveInstanceState(outState); } }
五、通过onCreateDialog()来快捷的建立对话框
我们上面建立的对话框都是用自定义布局的,难道我们之前学过的dialog知识都没用了么?我们如果没自定义对话框的需求,怎么办?就没有一种快一点的方式来建立对话框么?快用onCreatDialog吧!!!这个回调方法是DialogFragment独有的,通过它返回的是一个Dialog对象,这个对象就会被显示到屏幕上。千万别同时使用onCreatView和onCreatDialog方法,他们仅仅是为了完成同样一个目的的两条路而已。
PS:从生命周期的顺序而言,先执行onCreateDialog(),后执行onCreateView()
我在onCreatDialog建立一个警告对话框的builder,通过这个builder的create()方法来生成一个AlertDialog对象,因为AlertDialog是Dialog的子类,所以可以直接返回给Dialog。这里可以用其他不同对话框的builder,代码类似,只不过就是通过builder的creat()方法返回的是不同的对象而已。builder模式也是蛮巧妙的~
public class MyDialogFragment extends DialogFragment implements android.content.DialogInterface.OnClickListener{ @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle("用户申明") .setMessage(getResources().getString(R.string.hello_world)) .setPositiveButton("我同意", this) .setNegativeButton("不同意", this) .setCancelable(false); //.show(); // show cann't be use here return builder.create(); } @Override public void onClick(DialogInterface dialog, int which) { // TODO 自动生成的方法存根 } }
显示效果:
看到了么,这里的标题栏终于有用了,原来那个标题栏是为了给我们在这里用的啊~
注意:
① 因为这里创建的是一个dialog,所以用的onclickListener自然是对话框中的listener了。
② 千万别在构建对话框对象的时候顺手写了show()方法,我们现在是在fragment中初始化一个对话框,真正让他显示的时候是在activity中用这个dialogFragment对象显示的。如果这里写了show方法不会报错,但是会出现两个对话框!
那么,我们能不能在这里自定义对话框呢?当然可以啦,本身Dialog.builder就提供了自定义view的方法,和之前用Dialog一样自定义下viwe就搞定了。
@Override public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); // Get the layout inflater LayoutInflater inflater = getActivity().getLayoutInflater(); View view = inflater.inflate(R.layout.fragment_login_dialog, null); // Inflate and set the layout for the dialog // Pass null as the parent view because its going in the dialog layout builder.setView(view) // set your own view // Add action buttons .setPositiveButton("Sign in", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { } }).setNegativeButton("Cancel", null); return builder.create(); }
这里贴下我在另一篇文章的自定义对话框view的代码片段:
详细看这里:http://www.cnblogs.com/tianzhijiexian/p/3867731.html
/** * 自定义视图对话框 * * @param title */ public void viewDialog(String title) { // LayoutInflater是用来找layout文件夹下的xml布局文件,并且实例化 LayoutInflater factory = LayoutInflater.from(mContext); // 把activity_login中的控件定义在View中 View view = factory.inflate(R.layout.dialog_layout, null); // 将LoginActivity中的控件显示在对话框中 // 获取用户输入的“用户名”,“密码” // 注意:view.findViewById很重要,因为上面factory.inflate(R.layout.activity_login, // null)将页面布局赋值给了view了 TextView titleTv = (TextView) view .findViewById(R.id.dialog_textView_id); titleTv.setText(title); Button btn = (Button) view.findViewById(R.id.dialog_logout_button_id); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View arg0) { showToast("按下自定义视图的按钮了~"); } }); builder // 设定显示的View .setView(view); // 设置dialog是否为模态,false表示模态,true表示非模态 // ab.setCancelable(false); // 对话框的创建、显示,这里显示的位置是在屏幕的最下面,但是很不推荐这个种做法,因为距底部有一段空隙 AlertDialog dialog = builder.create(); Window window = dialog.getWindow(); window.setGravity(Gravity.BOTTOM); // 此处可以设置dialog显示的位置 window.setWindowAnimations(R.style.myAnimationstyle); // 添加动画 dialog.show(); }
六、DialogFragment与Activity之前进行通信
思路很简单,就是定义一个传输数据的接口,强制activity实现这个接口,在fragment需要传递数据的时候去调用这个接口的方法,activity就能在这个方法中得到相应的数据了。这点在之前的fragment传递数据中已经介绍过了,可以参考这篇文章:
http://www.cnblogs.com/tianzhijiexian/p/3888330.html
在真正项目中,fragment的编写并不需要了解activity的各类方法,好的编程风格是将fragment所涉及的方法以接口的方式封装起来,我在此写一个例子来说明一下。
1. 写一个接口——DataCallback
package com.kale.dialogfragmenttest; public interface DataCallback { public void getData(String data); }
2.activity实现这个接口
public class MainActivity extends Activity implements DataCallback{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); new MyDialogFragment() .show(getFragmentManager(), "dialog_fragment"); } @Override public void getData(String data) { // TODO 自动生成的方法存根 System.out.println("data = "+ data); } }
3.在DialogFragment中使用这个接口,并且用instanceof来看启动它的activity是否实现了这个接口,如果没实现就抛出异常。这样我们就能保证在大型项目中不会出现忘记实现这个接口的问题了。
import android.app.Activity; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.Dialog; import android.app.DialogFragment; import android.content.DialogInterface; import android.os.Bundle; public class MyDialogFragment extends DialogFragment implements android.content.DialogInterface.OnClickListener { @Override public void onAttach(Activity activity) { // onAttach()是合适的早期阶段进行检查MyActivity是否真的实现了接口。 // 采用接口的方式,dialog无需详细了解MyActivity,只需了解其所需的接口函数,这是真正项目中应采用的方式。 if (!(activity instanceof DataCallback)) { throw new IllegalStateException("fragment所在的Activity必须实现Callbacks接口"); } super.onAttach(activity); } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle("用户申明") .setMessage(getResources().getString(R.string.hello_world)) .setPositiveButton("我同意", this).setNegativeButton("不同意", this) .setCancelable(false); // show(); return builder.create(); } @Override public void onClick(DialogInterface dialog, int which) { // TODO 自动生成的方法存根 DataCallback callback = (DataCallback) getActivity(); callback.getData("test"); } }
此外fragment也可以通过fragment管理器,通过tag,获取其他fragment实例,从而进行fragment之间的通信。当然从编程思想的角度看,fragment之间的过多进行交叉调用,不利于程序的管控。
七、用DialogFragment实现再次弹窗
有时候我们可能有这样的需求,点击对话框中的一个按钮后又弹出一个对话框,这个该怎么做呢?首先在点击事件中将这个对话框在屏幕上移除,然后把这个fragment压栈,最后建立一个新的dialogFragment对象,show出来。我们虽然让这个fragment在屏幕上消失,但还是可以通过fragment管理器到回退栈中找到它。
二次弹窗的代码:
FragmentTransaction ft = getFragmentManager().beginTransaction(); /* * 如果不执行remove(),对话框即不会进入onDismiss()状态。会被显示在新的对话框下方,是可见的。 * 主要考虑美观的问题,如果下面的对话框大于上面的对话框就很难看了。 对于Dialog,container为0或者null。 */ ft.remove(this); /* * 将当前的PromptDialogFragment加入到回退堆栈,当用户按返回键,或者通过按帮助框的Close按钮dismiss帮助框是, * 重新显示提示框。 对于back stack的处理,系统具有一定的智能。例如:执行两次addToStackStack(),实际不会重复压栈。 * 有例如:注释掉remove()语句,即提示框不消失,而是在帮助框的下面。 * 但是在实验中发现是否有addToBackStack()都不会结果有影响,系统能够分析到对象存在,不需要压栈。没有去查源代码, * 猜测通过mBackStackId比对来进行智能处理。 */ ft.addToBackStack(null); new OhterDialogFragment() .show(getFragmentManager(), "dialog_fragment");
八、利用Fragment的特性,为不同屏幕做适配
如果我们想在大屏幕上显示对话框,而小屏幕中直接把对话框的内容放在activity中显示呢?
其实也很简单,本身这个dialogFragment就是一个fragment,所以完全有fragment的特性,你可以用fragmentTranscation将其放到任何布局中,你也可以用show()方法把它当作dialog显示出来。接下来就剩下一个问题了,判断屏幕大小。
在默认的values下新建一个bools.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <bool name="large_layout">false</bool> </resources>
然后,在res下新建一个values-large,在values-large下再新建一个bools.xml,通过加载不同的value就能知道是大屏还是小屏幕啦
<?xml version="1.0" encoding="utf-8"?> <resources> <bool name="large_layout">true</bool> </resources>
在代码中进行判断
public void showDialogInDifferentScreen(View view) { FragmentManager fragmentManager = getFragmentManager(); EditNameDialogFragment newFragment = new EditNameDialogFragment(); boolean mIsLargeLayout = getResources().getBoolean(R.bool.large_layout) ; Log.e("TAG", mIsLargeLayout+""); if (mIsLargeLayout ) { // The device is using a large layout, so show the fragment as a // dialog newFragment.show(fragmentManager, "dialog"); } else { // The device is smaller, so show the fragment fullscreen FragmentTransaction transaction = fragmentManager.beginTransaction(); // For a little polish, specify a transition animation transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); // To make it fullscreen, use the 'content' root view as the // container for the fragment, which is always the root view for the activity transaction.replace(R.id.id_ly, newFragment).commit(); } }
参考自:
http://blog.csdn.net/huangyabin001/article/details/30053835
http://blog.csdn.net/lmj623565791/article/details/37815413