07.21 控件-对话框与菜单
除了常规控件(07.01~07.19)和自定义 View(07.20),应用中还有一类特殊的界面元素——弹层:对话框、浮层、菜单和轻提示。它们不占据页面布局,而是在合适的时机"浮"在内容之上,用于确认操作、临时选择、快捷菜单和状态反馈。
先把弹层家族的全貌看清楚,再逐个展开:
# 一、AlertDialog 对话框
AlertDialog 是最常用的标准对话框,由 Material 风格的三段式构成:标题、内容区、按钮区。它不能直接实例化,通过 AlertDialog.Builder 构建。
# 1.1 基本对话框(标题 + 内容 + 按钮)
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("删除确认")
.setMessage("确定要删除这条记录吗?删除后无法恢复。")
.setPositiveButton("删除", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// which是按钮标识(POSITIVE_BUTTON/NEGATIVE_BUTTON/NEUTRAL_BUTTON)
deleteRecord();
}
})
.setNegativeButton("取消", null) // 传null:点击只关闭对话框
.show();
按钮最多三个,语义约定:
setPositiveButton:肯定操作(确定/删除/同意),显示在最右setNegativeButton:否定操作(取消/拒绝),显示在肯定键左侧setNeutralButton:中性操作(忽略/稍后),显示在最左
# 1.2 列表 / 单选 / 多选对话框
// 1. 普通列表:点击任意项后自动关闭
new AlertDialog.Builder(this)
.setTitle("选择颜色")
.setItems(new String[]{"红色", "绿色", "蓝色"}, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(MainActivity.this, "选中:" + which, Toast.LENGTH_SHORT).show();
}
})
.show();
// 2. 单选列表:默认选中项,需配合确定按钮提交
new AlertDialog.Builder(this)
.setTitle("选择性别")
.setSingleChoiceItems(new String[]{"男", "女", "保密"}, 0, // 0为默认选中索引
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.i(TAG, "当前选中:" + which);
// 注意:单选点击后默认不关闭,由确定按钮统一提交
}
})
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ListView list = ((AlertDialog) dialog).getListView();
Log.i(TAG, "提交选中:" + list.getCheckedItemPosition());
}
})
.show();
// 3. 多选列表:返回boolean[]记录各选项选中状态
new AlertDialog.Builder(this)
.setTitle("选择兴趣爱好")
.setMultiChoiceItems(new String[]{"运动", "阅读", "音乐", "旅行"},
new boolean[]{true, false, false, true}, // 初始选中状态
new DialogInterface.OnMultiChoiceClickListener() {
@Override
public void onClick(DialogInterface dialog, int which, boolean isChecked) {
Log.i(TAG, which + " 选中状态:" + isChecked);
}
})
.setPositiveButton("确定", null)
.show();
# 1.3 自定义布局对话框
内容区可以换成自己的布局(如登录框、输入框):
// 1. 加载自定义布局
View view = LayoutInflater.from(this).inflate(R.layout.layout_login_dialog, null);
EditText etName = view.findViewById(R.id.et_name);
EditText etPwd = view.findViewById(R.id.et_password);
// 2. setView设置内容区(自定义时不要再调setMessage,二者互斥)
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("登录")
.setView(view)
.setPositiveButton("登录", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
login(etName.getText().toString(), etPwd.getText().toString());
}
})
.setNegativeButton("取消", null)
.show();
常用控制方法:
dialog.show():显示(Builder 链式最后一步,或单独调用)dialog.dismiss():关闭对话框dialog.setCancelable(false):禁止点空白处/返回键关闭(强提醒场景)dialog.setOnCancelListener/setOnDismissListener:关闭时机回调
# 二、DialogFragment:对话框的推荐载体
直接持有 AlertDialog 引用有个经典坑:旋转屏幕(配置变更)时 Activity 重建,对话框消失。官方推荐用 DialogFragment 承载对话框——它本身是 Fragment,旋转后系统会自动恢复显示。
/**
* 确认对话框:用DialogFragment承载,旋转屏幕后依然存在.
*/
public class ConfirmDialogFragment extends DialogFragment {
// 返回要显示的对话框
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
String message = "";
if (getArguments() != null) {
message = getArguments().getString("message");
}
return new AlertDialog.Builder(requireContext())
.setTitle("提示")
.setMessage(message)
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 通过接口/ViewModel把结果传回宿主(避免直接持有Activity引用)
}
})
.setNegativeButton("取消", null)
.create();
}
// 对外提供展示入口(Fragment间传参用Bundle,规范做法)
public static ConfirmDialogFragment newInstance(String message) {
ConfirmDialogFragment fragment = new ConfirmDialogFragment();
Bundle args = new Bundle();
args.putString("message", message);
fragment.setArguments(args);
return fragment;
}
}
// Activity中展示
ConfirmDialogFragment.newInstance("确定删除吗?")
.show(getSupportFragmentManager(), "confirm");
DialogFragment 的优势:
- 配置变更(旋转屏)后对话框自动恢复
- 生命周期可感知,与 Activity/Fragment 体系一致
- 既可当对话框(onCreateDialog),也可当全屏弹层(onCreateView)
# 三、BottomSheetDialog 底部对话框
Material Design 风格的底部弹出面板,移动端常见的"分享面板""操作面板"场景:
// 需要Material组件库:implementation 'com.google.android.material:material:x.x.x'
BottomSheetDialog dialog = new BottomSheetDialog(this);
View view = LayoutInflater.from(this).inflate(R.layout.layout_share_sheet, null);
view.findViewById(R.id.item_wechat).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss(); // 点击选项后关闭
shareToWechat();
}
});
dialog.setContentView(view);
dialog.show();
配套布局 layout_share_sheet.xml(横向排列分享项):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="horizontal"
android:padding="20dp">
<LinearLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">
<ImageView android:id="@+id/item_wechat"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@mipmap/ic_wechat" />
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="微信" />
</LinearLayout>
<!-- 其他分享项同理,省略 -->
</LinearLayout>
# 四、PopupWindow 浮层
PopupWindow 是锚定某个 View 的轻量浮层,非模态(不遮罩不阻断),适合下拉筛选、气泡提示等场景:
// 1. 准备浮层内容
View popupView = LayoutInflater.from(this).inflate(R.layout.layout_filter_popup, null);
popupView.findViewById(R.id.item_price).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
filterByPrice();
mPopupWindow.dismiss();
}
});
// 2. 创建PopupWindow(宽高必须指定,否则不显示)
mPopupWindow = new PopupWindow(popupView,
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, true);
// true:点击外部区域关闭
mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
mPopupWindow.setOutsideTouchable(true);
// 3. 在锚点View下方显示(还有showAsDropDown的重载控制偏移)
mPopupWindow.showAsDropDown(anchorView);
// 或指定重力与偏移:showAtLocation(parent, Gravity.CENTER, 0, 0)
AlertDialog 与 PopupWindow 的核心区别:
| 对比项 | AlertDialog | PopupWindow |
|---|---|---|
| 模态 | 模态,阻断背后交互 | 非模态,背景可交互(可配置遮罩) |
| 位置 | 屏幕中央(Material 风格) | 锚定任意 View / 任意位置 |
| 生命周期 | 有 dismiss/cancel 回调 | 需手动管理,注意泄漏 |
| 典型场景 | 确认、选择 | 下拉筛选、气泡 |
PopupWindow 注意事项:不设置 setBackgroundDrawable 在部分机型上点击外部不关闭;在 Activity 销毁前要 dismiss(),否则可能造成窗口泄漏。
# 五、Toast 与 Snackbar 轻提示
# 5.1 Toast
系统级轻提示,短暂显示后自动消失,不占据页面布局:
// 标准用法:LENGTH_SHORT约2秒,LENGTH_LONG约3.5秒
Toast.makeText(context, "保存成功", Toast.LENGTH_SHORT).show();
注意事项:
- context 建议用 Application Context:在非 Activity 场景(如回调里)用 Activity context 可能泄漏;Android 11+ 后台自定义 Toast 被系统限制
- 自定义 View 的 Toast(
setView)在 Android 11+ 已被限制,推荐用 Snackbar 替代 - Toast 是队列机制:连续调用会排队依次显示,避免循环里刷 Toast
# 5.2 Snackbar
Material 风格的页面内提示,比 Toast 强在可携带操作按钮(典型场景:删除后提示"已删除"+ 撤销):
// 依赖Material组件库
Snackbar.make(findViewById(R.id.coordinator), "记录已删除", Snackbar.LENGTH_LONG)
.setAction("撤销", new View.OnClickListener() {
@Override
public void onClick(View v) {
undoDelete();
}
})
.show();
Toast vs Snackbar 选型:全局性、与页面布局无关的提示用 Toast;需要用户确认/撤销操作、希望与页面风格统一的用 Snackbar。
# 六、WindowManager 系统级悬浮窗
PopupWindow 只能在自己的 Activity 窗口内悬浮,而 WindowManager.addView 是向系统窗口层添加 View——它可以脱离 Activity 独立存在(Activity 关闭后仍在),悬浮在任何应用之上。全局悬浮球、视频小窗(画中画式)、来电浮层都是这个实现。
# 6.1 基本使用
public class FloatWindowService extends Service {
private WindowManager mWindowManager;
private View mFloatView;
@Override
public void onCreate() {
super.onCreate();
mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
// 1. 准备悬浮的View
mFloatView = LayoutInflater.from(this).inflate(R.layout.layout_float_window, null);
mFloatView.setOnTouchListener(new View.OnTouchListener() {
// 跟手拖动等交互(结合08.01的触摸事件处理)
@Override
public boolean onTouch(View v, MotionEvent event) {
return handleDrag(event);
}
});
// 2. 布局参数:type决定窗口层级,flags决定行为
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; // 8.0+的系统悬浮窗类型
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE // 不抢焦点(背后应用可继续操作)
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
params.format = PixelFormat.RGBA_8888;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.gravity = Gravity.START | Gravity.TOP;
params.x = 0;
params.y = 200;
// 3. 添加到窗口(异步线程操作WindowManager需要先Looper.prepare,Service回调已在主线程无此问题)
mWindowManager.addView(mFloatView, params);
}
@Override
public void onDestroy() {
super.onDestroy();
// 必须移除,否则窗口泄漏
if (mFloatView != null) {
mWindowManager.removeView(mFloatView);
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
# 6.2 悬浮窗权限(特殊权限)
系统悬浮窗需要 SYSTEM_ALERT_WINDOW 特殊权限,它不走普通的运行时权限申请,而是跳转系统设置页授权:
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
// 检查与引导授权(8.0+用canDrawOverlays判断)
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "请开启悬浮窗权限", Toast.LENGTH_SHORT).show();
startActivity(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getPackageName())));
}
# 6.3 与 PopupWindow 的本质区别
| 对比项 | PopupWindow | WindowManager 悬浮窗 |
|---|---|---|
| 窗口归属 | 依赖 Activity 窗口(token),Activity 关闭即消失 | 系统层独立窗口,跨应用存在 |
| 权限 | 无需权限 | SYSTEM_ALERT_WINDOW 特殊权限 |
| 生命周期 | 跟随页面 | 需自行管理(通常挂 Service) |
| 典型场景 | 页面内下拉/气泡 | 全局悬浮球、跨应用浮层、桌面歌词 |
注意事项:国内 ROM 对悬浮窗权限管控不一(部分需引导用户到厂商专属设置页);Service + WindowManager 是标准组合,注意 Service 被杀时清理窗口;8.0 以下旧 type(TYPE_PHONE 等)已废弃,统一用 TYPE_APPLICATION_OVERLAY。
# 七、Menu 菜单体系
菜单是"隐藏在按钮背后"的操作集合,分三类:
# 6.1 选项菜单(Options Menu)
Activity/Fragment 工具栏上的菜单(配合 ActionBar/Toolbar):
- 定义菜单资源:res/menu/menu_main.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_search"
android:title="搜索"
android:icon="@mipmap/ic_search"
app:showAsAction="always" />
<item android:id="@+id/action_settings"
android:title="设置"
app:showAsAction="never" />
</menu>
showAsAction:always 直接显示在工具栏、never 收进溢出菜单(三点)、ifRoom 有空间就显示。
- Activity 中创建与响应:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// 加载菜单资源
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.action_search:
openSearch();
return true; // 返回true表示已处理
case R.id.action_settings:
openSettings();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
# 6.2 上下文菜单(Context Menu)
长按某个 View 弹出的菜单(如列表项长按操作):
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo);
// 给目标View注册上下文菜单
registerForContextMenu(findViewById(R.id.tv_target));
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
getMenuInflater().inflate(R.menu.menu_context, menu);
}
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.action_copy:
copyText();
return true;
default:
return super.onContextItemSelected(item);
}
}
# 6.3 弹出式菜单(PopupMenu)
锚定任意 View 弹出的菜单(替代"长按"的显式入口,如点击"⋮"按钮):
PopupMenu popup = new PopupMenu(this, anchorView);
popup.getMenuInflater().inflate(R.menu.menu_context, popup.getMenu());
popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
Log.i(TAG, "点击了:" + item.getTitle());
return true;
}
});
popup.show();
# 八、常见问题与陷阱
- 旋转屏对话框消失:直接持有 AlertDialog 引用的场景,改用 DialogFragment 承载。
- setMessage 与 setView 互斥:同时设置时 setView 优先生效,内容文字会被覆盖。
- Builder.create() 与 show() 的区别:
show()内部会 create+显示;需要提前拿到 dialog 引用控制关闭时用create()+ 手动show()。 - PopupWindow 不显示:宽高必须是具体值(
WRAP_CONTENT/MATCH_PARENT或测量值),0是不显示的。 - Toast 内存泄漏:非 Activity 场景用 Application Context;页面销毁时的 Toast 已入队无法取消,属可接受的轻量问题,避免在 onDestroy 中再发 Toast。
- 对话框按钮点击后自动关闭:如需"校验失败不关闭对话框"(如输入校验),不能依赖 Builder 的按钮回调(默认必关闭),要么用
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener()覆盖,要么用 DialogFragment 自行控制。 - 悬浮窗窗口泄漏:WindowManager.addView 之后 Activity/Service 销毁前必须 removeView;悬浮窗内部点击跳转 Activity 需加
FLAG_ACTIVITY_NEW_TASK(Service 无任务栈)。 - 悬浮窗权限厂商差异:
Settings.canDrawOverlays为 false 时优先引导授权;部分国产 ROM 授权入口不同,需按厂商做兜底跳转。
# 结束
本节介绍了 Android 弹层与菜单体系:AlertDialog 的四种形态(基本/列表/单选多选/自定义布局)、官方推荐的 DialogFragment 载体(解决旋转屏恢复问题)、BottomSheetDialog 底部面板、PopupWindow 锚定浮层、Toast 与 Snackbar 的选型差异、WindowManager 系统级悬浮窗(跨应用窗口与悬浮窗权限),以及选项/上下文/弹出式三套菜单的实现。选型原则记住一张图(开篇总览):模态确认用 AlertDialog(DialogFragment 承载),锚定浮层用 PopupWindow,全局悬浮用 WindowManager,轻反馈用 Toast/Snackbar。更多内容请参考官方文档:对话框 (opens new window)、菜单 (opens new window) 与 悬浮窗权限 (opens new window)。