Android:弹出对话框方式梳理一览(一)
Guide|导言
在Android开发中,对话框可能是我们与用户交互的非常常用的方式,包括弹出一个小界面,可能在实际场景中都非常实用。本篇文章主要就是对Android弹出对话框的一些方式的梳理,同时也帮助我自己巩固,避免遗忘。
本文主要还是参考Google的官方文档,详见:对话框|Android开发者;
Dialog|对话框的基类
在Android中,Dialog类是对话框的基类,它负责实现对话框的一些共有属性,不过我们一般不直接使用Dialog类,而是使用它的衍生类,比如AlertDialog(可显示标题、最多三个按钮、可选项目列表或自定义布局的对话框),DatePickerDialog 或 TimePickerDialog(一个对话框,带有可让用户选择日期或时间的预定义界面)。
之前的文章中我们已经简单介绍过了Android中的Window相关机制,实际上Dialog也是通过Window机制显示出来的,我们可以简单看一眼源码(此处跳过也不影响后边内容):
Dialog(@UiContext @NonNull Context context, @StyleRes int themeResId,
boolean createContextThemeWrapper) {
......
//获取WindowManager服务
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//新创建一个Window用来显示对话框
final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
//将新创建出来的Window与WindowManager关联
w.setWindowManager(mWindowManager, null, null);
//设置弹出的位置为中心
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
一些重要的点已经注释在了代码中,可以看到,在Dialog的构造方法中就创建出来了一个Window来显示需要弹出的内容,所以说它本质上也是使用到了Window机制来进行内容的显示。接下来继续看它的show
方法的部分逻辑:
public void show() {
......
mWindowManager.addView(mDecor, l);
if (restoreSoftInputMode) {
l.softInputMode &=
~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
}
mShowing = true;
sendShowMessage();
}
这个show
方法就是最终显示对话框的方法,可以看到,这里显然是使用到了windowManager的addView
方法,所以到这里我们就可以明确其显示原理了,就是使用到了windowManagerService的功能。
Basic|基础使用
介绍完了Dialog的基本原理,接下来我们来了解Dialog的基础使用方法。使用Dialog大致可以分为五步:
- 创建Dialog的构造器
- 配置Dialog的内容
- [可选]进行一些生命周期配置(比如onStart,onStop等,此处可能在自定义Dialog中使用到)
- 显示Dialog
- 注销/隐藏Dialog
如果只是简单的使用,其实两步就可以概括:
- 创建Dialog
- 显示Dialog
比如一个最简单的示例如下所示:
//1.创建构造器
val dialogBuilder = AlertDialog.Builder(this)
//2.配置dialog内容
dialogBuilder.apply {
//配置正文内容
setMessage("This is the Message!")
//配置标题
setTitle("This is a Dialog!")
}
//3.创建并显示dialog
dialogBuilder.create().show()
这里注意似乎语法糖有一些问题,比如这样写:
//1.创建构造器
val dialogBuilder = AlertDialog.Builder(this)
//2.配置dialog内容
dialogBuilder.apply {
//配置正文内容
setMessage("This is the Message!")
//配置标题 -- 使用语法糖将会导致失效
title = "This is a Dialog!"
}
//3.创建并显示dialog
dialogBuilder.create().show()
配置出来的标题就会失效,因为这个语法糖关联的方法的是Activity的方法,目前建议还是先别用语法糖。如果需要添加按钮,也按照上面这个方式来添加即可。
添加按钮&Message
对于AlertDialog来说,最多可以添加三个按钮,性质分别是Positive
,Negative
,Neutral
,官方意义上可以理解成确认,取消,中立三个意思。比如说申请权限时的授权
,拒绝授权
,仅在使用中允许
就差不多可以对应上前面的三个意思。
对于Positive性质的按钮,我们可以调用setPositiveButton(CharSequence text, final OnClickListener listener)
方法来设置,比如:
setPositiveButton("确认",object :DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface?, which: Int) {
Log.d(TAG, "onClick: 确认")
}
})
不过,我们当然也可以替换成Lambda表达式:
setNegativeButton("取消") {dialog,which->
Log.d(TAG, "onCreate: 取消")
}
我们来改进上面这段代码,最终为:
//1.创建构造器
val dialogBuilder = AlertDialog.Builder(this)
//2.配置dialog内容
dialogBuilder.apply {
//配置正文内容
setMessage("This is the Message!")
//配置标题
setTitle("This is a Dialog!")
//设置按钮
//确认
setPositiveButton("确认",object :DialogInterface.OnClickListener{
override fun onClick(dialog: DialogInterface?, which: Int) {
Log.d(TAG, "onClick: 确认")
}
})
//取消
setNegativeButton("取消"){dialog,which->
Log.d(TAG, "onCreate: 取消")
}
//中立
setNeutralButton("中立"){dialog,which ->
Log.d(TAG, "onCreate: 中立")
}
}
//3.创建并显示dialog
dialogBuilder.create().show()
最终显示的效果:
添加一般列表
除了一般的按钮和文本之外,我们还可以向对话框内填入列表,不过由于列表显示在对话框的内容区域中,因此对话框无法同时显示消息和列表。
添加列表项的最简单的方法通过setItems
方法:
setItems(arrayOf("列表一","列表二","列表三"),object :DialogInterface.OnClickListener{
override fun onClick(dialog: DialogInterface?, which: Int) {
Log.d(TAG, "index:${which} ,dialog:${dialog}")
}
})
其中dialog参数为弹出的Dialog对象,which参数为之前传入的String数组的索引值,需要说明的是这个Dialog应该是不会被复用的,是一个非永久的Dialog,我们可以通过打印出来的日志看出来:
理解了上边的代码后,可以进一步简化为:
setItems(arrayOf("列表一","列表二","列表三"),{dialog, which ->
Log.d(TAG, "index:${which} ,dialog:${dialog}")
})
实现的效果如下:
另外,也可以使用实现了ListAdapter接口的Adapter来设置列表项,比如说:
setAdapter(ArrayAdapter(context,
com.google.android.material.R.layout.support_simple_spinner_dropdown_item,
arrayOf("1","2")
), object :DialogInterface.OnClickListener{
override fun onClick(dialog: DialogInterface?, which: Int) {
Log.d(TAG, "index:${which} ,dialog:${dialog}")
}
})
添加永久性列表
前边提到过我们添加的DIalog不是一个可复用的,而是每次弹出都会创建一个新的Dialog,接下来我们来介绍可以添加永久性列表的方法。
添加永久性复选框☑️
所谓复选框,就是可以同时选择多个列表项的Dialog,它的添加方法和之前的也类似,我们可以调用setMultiChoiceItems
来设置,比如:
setMultiChoiceItems(arrayOf("Item1","Item2","Item3"), booleanArrayOf(true,false,true),
object :DialogInterface.OnMultiChoiceClickListener {
override fun onClick(
dialog: DialogInterface?,
which: Int,
isChecked: Boolean
) {
Log.d(TAG, "dialog is ${dialog},index is ${which},is checked? ${isChecked}")
}
}
)
这个方法相比之前添加列表的参数中多了一个 BooleanArray 类型,该参数指定的是第一次弹出复选框时的列表选择状态,比如说这里我们传入的是 true,false,true
的参数,最终第一次弹框的效果就是:
如果想一开始什么都不选中,那传入一个null值即可。
最后我们来验证一下该Dialog是否是一个永久性的,分别点击列表项,我们可以发现每次打印的Dialog都是同一项:
说明这确实是一个永久性的Dialog。
添加永久性的单选框
最后就是添加一个永久性的单选框,这个其实和一开始的添加一个非永久性的单选框的方法很类似,唯一的一点就是多了一个参数来指定第一次弹出时选中的列表项,跟之前的类似:
setSingleChoiceItems(arrayOf("item1","item2","item3"),1,
object :DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface?, which: Int) {
Log.d(TAG, "index:${which} ,dialog:${dialog}")
}
}
)
为Dialog添加自定义内容
到之前为止其实都是Dialog的很基础的应用,实际上在使用过程中我们可能需要弹出一个Dialog来展示我们自己的定制化的内容,比如说一个登录页面,这个时候我们就不能用之前的简单的方法了,取而代之,我们可以调用setView
方法来为我们的Dialog填充自定义的内容,这个过程实际上类似于动态添加View的过程。
举例来说,我们可以先设计一个布局来描述我们需要填充的具体内容:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/header_img"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
app:srcCompat="@drawable/images" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="password" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/user_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="user" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
这里用了一个线性布局来描述,至于为什么不用更好用的约束布局,因为我测试过发现直接丢一个约束布局可能会带来显示异常的问题,所以如果想使用约束布局的话可能需要在约束布局外边包一层线性布局。
然后我们来配置这个自定义的Dialog:
//1.创建构造器
val dialogBuilder = AlertDialog.Builder(this)
//2.配置dialog内容
dialogBuilder.apply {
//添加自定义的布局
setView(R.layout.dialog_layout)
setNegativeButton("Cancel",object :DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface?, which: Int) {
}
})
setPositiveButton("Confirm",object : DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface?, which: Int) {
}
})
}
//3.创建并显示dialog
dialogBuilder.create().show()
最终显示的效果就是这样的:
我们可以和官方文档上实现的效果进行对比:
可以发现我们的按钮是比较丑的,实际上这部分我们也可以直接做进我们的自定义布局里:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/header_img"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
app:srcCompat="@drawable/images" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="password" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/user_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="user" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/positivebtn"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_margin="0dp"
android:layout_weight="1"
android:background="#C8E6C9"
android:elevation="0dp"
android:text="Confirm" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/negbtn"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_margin="0dp"
android:layout_weight="1"
android:background="#FFCDD2"
android:elevation="0dp"
android:text="Cancel" />
</LinearLayout>
</LinearLayout>
最终的效果就好上很多:
至于我们具体的点击事件的设置,则可以通过布局膨胀器获取具体的View来为按钮设置点击事件监听 :
//2.配置dialog内容
dialogBuilder.apply {
val customView = layoutInflater.inflate(R.layout.dialog_layout,null,false)
setView(customView)
customView.findViewById<Button>(R.id.negbtn).setOnClickListener {
Toast.makeText(context, "cancel!", Toast.LENGTH_SHORT).show()
}
customView.findViewById<Button>(R.id.positivebtn).setOnClickListener {
Toast.makeText(context, "load!", Toast.LENGTH_SHORT).show()
}
}
//3.创建并显示dialog
dialogBuilder.create().show()
为Dialog添加进出场动画
在使用其他App时,我们往往会发现弹出的Dialog并不都是直接闪现出在屏幕中间的,经常会有一个从底部或者是从左右弹出的动画效果,实现该效果的步骤也很简单,不过我们需要有一些Android动画的基础,我们在anim文件夹下新建弹入和弹出动画:
//弹入动画
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromYDelta="100%p" android:toYDelta="0"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>
//弹出动画
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromYDelta="0" android:toYDelta="100%p"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>
之后我们在styles文件下新建style属性:
<style name="BottomAni">
<item name="android:windowEnterAnimation">@anim/pop_from_bottom</item>
<item name="android:windowExitAnimation">@anim/exit_from_bottom</item>
</style>
最后在创建完Dialog时指定window动画属性:
viewBinding.button.setOnClickListener {
//1.创建构造器
val dialogBuilder = AlertDialog.Builder(this)
//2.配置dialog内容
dialogBuilder.apply {
val customView = layoutInflater.inflate(R.layout.dialog_layout,null,false)
setView(customView)
customView.findViewById<Button>(R.id.negbtn).setOnClickListener {
Toast.makeText(context, "cancel!", Toast.LENGTH_SHORT).show()
}
customView.findViewById<Button>(R.id.positivebtn).setOnClickListener {
Toast.makeText(context, "load!", Toast.LENGTH_SHORT).show()
}
}
//3.创建并显示dialog
dialogBuilder.create().apply {
//指定窗口动画
window?.attributes?.windowAnimations = R.style.BottomAni
window?.setGravity(Gravity.BOTTOM)
}.show()
最终效果: