一.前言
在我看来,DialogFragment可以帮助我们非常方便地完成自定义弹窗,随心所欲的控制弹窗出现的位置,出现动画等等。甚至可以处理一些复杂的业务,同时拥有Dialog和Fragment的所有特点。可以轻量地用于一个loading,也可以重业务的处理一些很复杂的逻辑。使用起来非常的方便,现在我的项目中的对话框全部采用DialogFragment,后续可能会用来做一些侧滑菜单或者上拉抽屉等效果。
二.基本用法
所有的DialogFragment的使用都分为以下最基础的三步:
第一步:创建对话框布局文件layout_first_dialog.xml
layout_first_dialog.xml
内容:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="280dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/bg_ffffff_r15"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingTop="20dp">
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="您好,这里是第一个Dialog"
android:textSize="20sp"
android:textStyle="bold" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="20dp"
android:background="#999999" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_close"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:layout_weight="1"
android:gravity="center"
android:text="取消"
android:textSize="18sp" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="#999999" />
<TextView
android:id="@+id/tv_confirm"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_gravity="bottom"
android:layout_weight="1"
android:gravity="center"
android:text="确定"
android:textColor="#3085CE"
android:textSize="18sp" />
</LinearLayout>
</LinearLayout>
布局非常简单 , 没什么好说的。只有一点要说明,根布局里虽然设置了android:layout_width="280dp"
预览的效果图感觉也没问题。但是!!但是这里设置的宽度在真正使用的时候是无效的,只是让开发过程中预览图看起来没那么奇怪。在真正使用的时候,如果不处理,会被覆盖为android:layout_width="wrap_content"
,android:layout_height
也是同样的道理。这个后边会说,过。
第二步: 创建一个继承与DialogFragment的类FirstDialogFragment
内容如下:
class FirstDialogFragment : DialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
//确定Dialog布局
return inflater.inflate(R.layout.layout_first_dialog, container, false)
}
}
这是最简单的用法,仅仅是在onCreateView中
确定了DialogFragment的布局。
之后会扩展,暂时先这样。
第三步:在Activity中展示DialogFragment
这里我创建了一个方法showFirstDialog()
,直接设置一个点击事件,调用方法就可以了。
private fun showFirstDialog() {
val firstDialog = FirstDialogFragment()
firstDialog.showNow(supportFragmentManager, "FirstDialogFragment")
}
这里我们直接创建了一个对象firstDialog
,然后调用showNow
方法就可以展示弹窗了,showNow
方法需要两个参数,一个是supportFragmentManager
这个每个Activity都有,另一个就是tag
,类型是一个字符串,用于给弹窗打上tag,进行标记,在弹窗比较多时方便管理,若不需要可以随意传一个字符串。
(这里还有一个show
方法也可以展示弹窗, 但尽量不使用,之后会讲到)
效果如下:
可以看到我们在布局中设置的
android:layout_width="280dp"
确实没有生效。解决方式请看下个模块
三.Dialog的一些基本使用技巧
3.1 DialogFragment的生命周期
仔细看源码我们可以发现DialogFragment的生命周期回调是真的多,同时包含了Fragment和Dialog的生命周期,而且命名又非常接近。但其实我们只需要关注四个就可以了:
分别是:
onCreateView
:确认DialogFragment布局
onActivityCreated
:DialogFragment在Activity中创建完毕,马上准备展示
onResume
:弹窗展示
onDismiss
: 弹窗消失
class FirstDialogFragment : DialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Log.d("FirstDialogFragment","onCreateView")
//确定Dialog布局
return inflater.inflate(R.layout.layout_first_dialog, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
Log.d("FirstDialogFragment","onActivityCreated")
}
override fun onResume() {
super.onResume()
Log.d("FirstDialogFragment","onResume")
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
Log.d("FirstDialogFragment","onDismiss")
}
}
3.2 设置Dialog弹窗表现(大小,背景,位置)
在之前的例子中,我们可以看到,之前设置的android:layout_width="280dp"
没有生效,而是被覆盖成了android:layout_width="wrap_content"
,这导致弹窗非常丑,而我们的layout_hight恰好就是wrap_content,所以竖直方向上看不出来影响。
那么现在解决方案有两个:
一:
修改布局文件,使布局在android:layout_width="wrap_content"
的情况下也很好看,就像竖直方向上那样。(这个很简单 不需要讲,过)
二:
修改弹窗窗口大小,不让android:layout_width被覆盖为wrap_content
修改弹窗大小的方式如下,我们在FirstDialogFragment创建方法initWindow()
并在onActivityCreated
中调用
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
Log.d("FirstDialogFragment", "onActivityCreated")
initWindow()
}
private fun initWindow() {
//初始化window相关表现
val window = dialog?.window
//设备背景为透明(默认白色)
window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
//设置window宽高(单位px)
window?.attributes?.width = 700
//window?.attributes?.height = 350
//设置window位置
window?.attributes?.gravity = Gravity.CENTER//居中
}
注意,设置window宽高的单位是px,280dp大概相当于700px,我没有验证过,这种单位转换可以通过一些工具类实现,网上有很多,这里为了方便,就直接赋值700了。当然,也可以设置高度。
另外,我们可以设置window的背景颜色,根据系统的不同,有些window的默认背景是黑色,有些是白色,这会导致我们在布局文件根布局中设置是窗口背景android:background="@drawable/bg_ffffff_r15"
效果不如预期。所以通常我会将window背景设置为透明。
最后我们可以设置对话框出现的位置,默认是Activity居中,但是也可以设置成底部,顶部,上下左右都可以,这为我们做抽屉效果提供了思路。
设置了window之后,效果如下:
圆角有些大,但宽高没那么奇怪了
3.3 设置Dialog的点击事件和业务处理
在这之前我们是将Dialog展示了出来,但是点击两个按钮并没有效果,所以这里就需要设置一下业务逻辑。
创建initView方法,并且在onActivityCreated中调用如下:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
Log.d("FirstDialogFragment", "onActivityCreated")
initWindow()
initView()
}
private fun initView() {
//设置监听
tv_cancel.setOnClickListener {
//具有Fragment的一切特性 可以获取依赖的activity对象
Toast.makeText(activity, "点击了取消", Toast.LENGTH_SHORT).show()
dismiss()
}
tv_confirm.setOnClickListener {
Toast.makeText(activity, "点击了确定", Toast.LENGTH_SHORT).show()
dismiss()
}
}
运行一下就可以看到效果了
3.4 DialogFragment和Activity的交互
交互分为可以有很多方式实现,就像Activity和Fragment那样也可以,或者直接通过对象调用,或者通过接口回调,想象力不要被限制了。
例子1:直接通过对象调用
Activity中调用DialogFragment方法:
因为在Activity创建了对象,所以可以直接调用DialogFragment的公开方法
DialogFragment中调用Activity方法:
DialogFragment具有Fragment的一切特性,可以获取依赖的activity对象,再通过as
关键字可以转化为具体的activity对象。
例子2:通过接口回调
上边的直接调用可能不怎么优雅,可以看看这个例子:
我在FirstDialogFragment新建一个接口,并且建立对象,在点击的时候回调:
var clickCallBack: ClickCallBack? = null
private fun initView() {
//设置监听
tv_cancel.setOnClickListener {
clickCallBack?.clickCancel()
dismiss()
}
tv_confirm.setOnClickListener {
clickCallBack?.clickConfirm()
dismiss()
}
}
fun setCallBack(callBack: ClickCallBack) {
clickCallBack = callBack
}
interface ClickCallBack {
fun clickCancel()
fun clickConfirm()
}
最后再在MainActivit中调用setCallBack方法传入匿名对象设置回调:
private fun showFirstDialog() {
val firstDialog = FirstDialogFragment()
firstDialog.showNow(supportFragmentManager, "FirstDialogFragment")
firstDialog.setCallBack(object :FirstDialogFragment.ClickCallBack{
override fun clickCancel() {
Toast.makeText(this@MainActivity, "点击了取消", Toast.LENGTH_SHORT).show()
}
override fun clickConfirm() {
Toast.makeText(this@MainActivity, "点击了取消", Toast.LENGTH_SHORT).show()
}
})
}
四.DialogFragment的一些设置项以及坑
4.1 show和showNow的注意点
首先说一下结论:在使用过程中尽量使用showNow
现在我们知道:
firstDialog.show(supportFragmentManager, "First")
和
firstDialog.showNow(supportFragmentManager, "First")
都可以展示dialog,
但在使用它们过程中还有需要注意的地方:
- 1.show要比showNow稍微“慢”一点,这导致调用show了后,立刻修改dialog中的view(例如textView修改字符内容)会崩溃,而showNow不会
先在FirstDialogFragment中添加方法:
fun setContent(text: String) {
tv_content.text = text
}
以下代码会崩溃:
val firstDialog = FirstDialogFragment()
firstDialog.show(supportFragmentManager, "First")
firstDialog.setContent("Hello")
以下代码则正常执行(对话框内容被修改为Hello):
val firstDialog = FirstDialogFragment()
firstDialog.showNow(supportFragmentManager, "First")
firstDialog.setContent("Hello")
- 2 (废弃)展示弹窗后fragment对象会添加到activity,showNow会在弹窗dismiss消失后移除fragment,show不会移除。
(以前同一个对象非连续地调用两次show会崩溃,现在不会了,可能是google更新了,使show也在弹窗消失后移除了) - 3 不可连续地调用show或者showNow
这个“连续”是指在弹窗还没有消失的时候再次调用
原因其实在2中说了,展示弹窗后fragment对象会添加到activity,而同一个fragment只能添加一次,所以连续调用会崩。
一下代码会崩溃:
firstDialog.showNow(supportFragmentManager, "First")
firstDialog.showNow(supportFragmentManager, "First")
firstDialog.show(supportFragmentManager, "First")
firstDialog.show(supportFragmentManager, "First")
避免方法也很简单,用isResumed
来判断当前dialog是否正在展示
private fun showFirstDialog() {
if (firstDialog.isResumed) {
return
}
firstDialog.showNow(supportFragmentManager, "First")
}
当然也可以不直接return,可以做一些其他业务处理
4.2 使Dialog不可消失
点击返回键弹窗区域外均不消失
方式一(推荐):
必须在showNow之后才有效
firstDialog.showNow(supportFragmentManager, "First")
firstDialog.dialog?.setCancelable(false) //必须在showNow之后才有效
方式一:
任何时候都生效
firstDialog.isCancelable=false
——————————————————————————————
点击弹窗区域外不消失 点击返回键消失
firstDialog.dialog?.setCanceledOnTouchOutside(false)
——————————————————————————————
点击弹窗区域外不消失 点击返回键直接销毁界面
思路:设置弹窗消失监听,在消失时直接finish界面
firstDialog.dialog?.setOnDismissListener {
finish()
}
扩展一下,不光可以设置弹窗消失监听,还可以设置弹窗展示监听firstDialog.dialog?.setOnShowListener
认真看到这里,你已经可以通过DialogFeagment在你的项目中大显神威了,接下来是比较复杂的DialogFragment扩展用法。
五.DialogFragment的扩展用法
...未完待续...
1.设置出现和消失动画
2.侧边栏抽屉等效果