Flutter进阶(8):全局浮层系统(Overlay)
Flutter中的 Overlay 是一种用于在应用程序的 UI 堆栈上显示临时内容的机制。它允许开发者在不影响现有 UI 结构的情况下,将新的 UI 元素叠加在现有内容之上。以下是 Overlay 的详细使用,包含基础到高级的用法。
一、Overlay 基础概念
Overlay 是一个可以包含多个叠加层的 Stack 栈结构,每个层都可以独立地显示和隐藏。这些层可以是模态对话框、提示框、加载指示器等。这些界面始终显示在屏幕最上层。Overlay 是一个特殊的 Stack,它由 OverlayEntry 组成,每个 OverlayEntry 代表一个浮动层。Flutter 应用中默认有一个全局的 Overlay,可以通过 Overlay.of(context) 获取。
优点
- 灵活性:Overlay 允许在任何地方动态添加和移除 UI 元素,而不需要修改现有的 Widget 树。
- 非侵入性:由于 Overlay 层是独立于主 UI 树的,它们不会干扰到其他部分的布局和交互。
- 易于管理:通过 OverlayEntry 对象,可以方便地对每个叠加层进行控制,如显示、隐藏和更新。
缺点
- 需要手动管理 OverlayEntry,增加了代码复杂度。
- 不当的管理可能导致内存泄漏。
类型
Flutter 中的 Overlay 主要分为两种类型:
- 系统级 Overlay:由 Flutter 框架自动管理的,如路由动画、提示框等。
- 自定义 Overlay:开发者可以根据需要创建和管理的 Overlay 层。
应用场景
- 模态对话框:用于显示需要用户关注的提示信息或操作确认。
- 加载指示器:在执行耗时操作时,显示加载状态给用户。
- 悬浮工具栏:在屏幕边缘显示一些常用功能的快捷方式。
- 弹出菜单:点击某个元素后,显示相关的选项列表。
二、基本使用流程
先介绍一下 Overlay 的核心类:
- Overlay(叠加层控件):Flutter中的 Overlay 控件是一个 Widget,它用于创建和管理叠加层。它本身不可见,但可以包含多个可见的 OverlayEntry。
- OverlayEntry(叠加层条目):OverlayEntry 是 Overlay 中的条目,它代表了要在叠加层上显示的内容。每个 OverlayEntry 都可以包含一个 Widget,并定义了在叠加层上的位置和大小。
- OverlayState(叠加层状态):OverlayState 是与 Overlay 关联的状态对象,它用于添加、移除和管理 OverlayEntry。可以通过 BuildContext 的方法来获取 OverlayState 对象。
另外:
-
添加和移除OverlayEntry:使用 OverlayState 对象的方法,可以将 OverlayEntry 添加到 Overlay 中,并通过调用 remove 方法将其从 Overlay 中移除。
-
OverlayEntry的位置和大小:OverlayEntry 可以通过设置 Positioned 或 Align 等控件来确定其在叠加层上的位置和大小。可以使用 top、bottom、left、right 等属性来定位 OverlayEntry。
(1)获取 Overlay 的三种方式
// 方式1:通过 BuildContext 获取
OverlayState overlayState = Overlay.of(context);
// 方式2:通过 GlobalKey 获取
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
OverlayState overlayState = navigatorKey.currentState!.overlay;
// 方式3:通过 Navigator 获取
OverlayState overlayState = Navigator.of(context).overlay!;
(2)创建OverlayEntry
// 创建 OverlayEntry
OverlayEntry entry = OverlayEntry(
builder: (context) => Positioned(
top: 100,
left: 50,
child: Material(
elevation: 4,
child: Container(
padding: EdgeInsets.all(16),
child: Text('浮动内容'),
),
),
),
// 可选参数
opaque: false, // 是否不透明
maintainState: true, // 是否保持状态
);
(3)插入 OverlayEntry
// 插入到 Overlay
overlayState.insert(entry);
// 不创建 OverlayState 的方式,直接插入到 Overlay
Overlay.of(context).insert(entry);
实际使用中,也可以选择不创建 OverlayState,而是直接使用 Overlay.of(context).insert(entry); 来插入 Overlay。因为 OverlayState overlayState = Overlay.of(context);。
(4)移除 OverlayEntry
// 直接移除
entry.remove();
// 延时3秒移除
Future.delayed(const Duration(seconds: 3), () {
entry.remove();
});
三、实战案例
3.1 案例1:显示悬浮按钮
这个例子要实现点击一个按钮会打开 Overlay 显示一个 FloatingActionButton(使用了OverlayState):
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: OverlayExample(), // 使用 OverlayExample 作为主页
);
}
}
class OverlayExample extends StatelessWidget {
const OverlayExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Overlay 示例'),
),
body: Center(
child: ElevatedButton(
child: const Text('显示Overlay'),
onPressed: () {
showFloatingButtonOverlay(context);
},
),
),
);
}
}
void showFloatingButtonOverlay(BuildContext context) {
OverlayState? overlayState = Overlay.of(context);
late OverlayEntry overlayEntry;
// 创建 OverlayEntry
overlayEntry = OverlayEntry(
builder: (BuildContext context) {
return Positioned(
top: 100,
right: 16,
child: FloatingActionButton(
onPressed: () {
// 悬浮按钮被点击
debugPrint('Floating Button Clicked');
overlayEntry.remove(); // 移除 OverlayEntry
},
child: const Icon(Icons.add),
),
);
},
);
// 将 OverlayEntry 添加到 Overlay 中
overlayState?.insert(overlayEntry);
}
运行起来的效果如图:

点击 "显示Overlay按钮" 后的效果图如下:

3.2 案例2:显示悬浮提示框
要使用 Overlay,我们需要通过 OverlayEntry 来定义每一个悬浮的 widget,并将它们插入到 Overlay 中。
以下是一个简单的示例,展示了如何使用 Overlay 来显示一个悬浮的提示框:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Overlay 示例'),
),
body: const OverlayExample(),
),
);
}
}
class OverlayExample extends StatefulWidget {
const OverlayExample({super.key});
@override
// ignore: library_private_types_in_public_api
_OverlayExampleState createState() => _OverlayExampleState();
}
class _OverlayExampleState extends State<OverlayExample> {
OverlayEntry? _overlayEntry; // OverlayEntry对象定义
@override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: () {
_showOverlay(context); // 按钮点击,显示Overlay弹窗
},
child: const Text('显示Overlay'),
),
);
}
// Overlay显示函数
void _showOverlay(BuildContext context) {
// 创建OverlayEntry
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
top: 100,
left: 300,
child: Material(
color: Colors.transparent,
child: Container(
color: Colors.black54,
width: 200,
height: 100,
child: const Center(
child: Text(
'这是一个Overlay',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
),
),
),
);
// 不创建 OverlayState 的方式,直接插入OverlayEntry到Overlay中
Overlay.of(context).insert(_overlayEntry!);
// 3秒后移除OverlayEntry
Future.delayed(const Duration(seconds: 3), () {
_overlayEntry?.remove();
_overlayEntry = null;
});
}
}
在上述代码中,我们通过创建一个 OverlayEntry 并使用 Overlay.of(context)?.insert(_overlayEntry!) 将其插入到 Overlay 中。3 秒后,我们移除了这个 OverlayEntry。
运行起来的效果如图:

点击 "显示Overlay按钮" 后的效果图如下:

3.3 案例3:显示全局 Toast 组件
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Overlay 示例'),
),
body: const OverlayExample(),
),
);
}
}
class OverlayExample extends StatefulWidget {
const OverlayExample({super.key});
@override
// ignore: library_private_types_in_public_api
_OverlayExampleState createState() => _OverlayExampleState();
}
class _OverlayExampleState extends State<OverlayExample> {
@override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: () {
Toast.show(context, '操作成功'); // 按钮点击,显示Overlay弹窗
},
child: const Text('显示Overlay'),
),
);
}
}
// 全局 Toast 组件实现
class Toast {
static void show(BuildContext context, String message) {
final overlay = Overlay.of(context);
final overlayEntry = OverlayEntry(
builder: (context) => Positioned(
top: 130,
left: MediaQuery.of(context).size.width * 0.4 + 10,
child: Material(
color: Colors.transparent,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(20),
),
child: Text(
message,
style: const TextStyle(color: Colors.white),
),
),
),
),
);
overlay.insert(overlayEntry);
Future.delayed(const Duration(seconds: 2)).then((_) {
overlayEntry.remove();
});
}
}
运行起来的效果如图:

点击 "显示Overlay按钮" 后的效果图如下:

3.4 案例3:显示自定义弹出菜单
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Overlay 示例'),
),
body: const OverlayExample(),
),
);
}
}
class OverlayExample extends StatefulWidget {
const OverlayExample({super.key});
@override
// ignore: library_private_types_in_public_api
_OverlayExampleState createState() => _OverlayExampleState();
}
class _OverlayExampleState extends State<OverlayExample> {
@override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: () {
showContextMenu(context, const Offset(300, 100)); // 按钮点击,显示Overlay弹窗
},
child: const Text('显示Overlay'),
),
);
}
}
// 自定义弹出菜单
void showContextMenu(BuildContext context, Offset tapPosition) {
final overlay = Overlay.of(context);
final renderBox = context.findRenderObject() as RenderBox;
renderBox.localToGlobal(Offset.zero);
OverlayEntry? entry;
entry = OverlayEntry(
builder: (context) => GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => entry?.remove(),
child: Stack(
children: [
Positioned(
left: tapPosition.dx,
top: tapPosition.dy,
child: Material(
elevation: 8,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.7,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildMenuItem('复制', Icons.copy),
_buildMenuItem('分享', Icons.share),
_buildMenuItem('删除', Icons.delete),
],
),
),
),
),
],
),
),
);
overlay.insert(entry);
}
Widget _buildMenuItem(String text, IconData icon) {
return InkWell(
onTap: () {
debugPrint('选择了: $text');
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
width: 160,
child: Row(
children: [
Icon(icon, size: 20),
const SizedBox(width: 8),
Text(text),
],
),
),
);
}
运行起来的效果如图:

点击 "显示Overlay按钮" 后的效果图如下:

3.5 案例5:显示可拖拽浮动按钮
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Overlay 示例'),
),
body: const DraggableOverlayButton(),
),
);
}
}
class DraggableOverlayButton extends StatefulWidget {
const DraggableOverlayButton({super.key});
@override
// ignore: library_private_types_in_public_api
_DraggableOverlayButtonState createState() => _DraggableOverlayButtonState();
}
class _DraggableOverlayButtonState extends State<DraggableOverlayButton> {
late OverlayEntry _entry;
Offset _position = const Offset(300, 200);
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_showOverlay();
});
}
void _showOverlay() {
_entry = OverlayEntry(
builder: (context) => Positioned(
left: _position.dx,
top: _position.dy,
child: GestureDetector(
onPanUpdate: (details) {
setState(() {
_position += details.delta;
_entry.markNeedsBuild();
});
},
child: Material(
elevation: 8,
shape: const CircleBorder(),
child: FloatingActionButton(
onPressed: () {
debugPrint('浮动按钮点击');
},
child: const Icon(Icons.add),
),
),
),
),
);
Overlay.of(context).insert(_entry);
}
@override
void dispose() {
_entry.remove();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(); // 空容器,实际内容在 Overlay 中
}
}
效果图如下所示:

四、全局模态窗口完整实现实例
下面我将提供一个完整的、可直接使用的全局模态窗口实现,包含拖拽功能、自适应尺寸等特性。先看下效果图:

功能特性
-
多种显示方式:
- 默认居中显示
- 可自定义尺寸和位置
- 支持全屏显示
-
交互功能:
- 点击外部关闭(可配置)
- 拖拽移动窗口(可配置)
- 内置关闭按钮
-
视觉效果:
- 阴影和圆角效果
- 半透明背景遮罩
-
状态管理:
- 自动管理 OverlayEntry
- 防止重复打开
- 全局关闭方法
(1)创建全局模态窗口管理器
文件名 global_manager.dart:
import 'package:flutter/material.dart';
class GlobalModal {
static OverlayEntry? _currentEntry;
/// 显示全局模态窗口
static void show({
required BuildContext context, // 窗口上下文
required Widget child, // 窗口内容
Size? size, // 窗口尺寸
Offset? position, // 窗口左上角坐标
Color barrierColor = Colors.black54, // 背景色
bool barrierDismissible = true, // 点击背景是否关闭
bool draggable = false, // 是否允许拖拽
Duration animationDuration = const Duration(milliseconds: 300),
Curve animationCurve = Curves.easeOutBack,
}) {
// 如果已有窗口显示,先关闭
if (_currentEntry != null) {
_currentEntry?.remove();
_currentEntry = null;
}
final overlayState = Overlay.of(context);
final mediaQuery = MediaQuery.of(context);
// 默认尺寸为屏幕的80%
final modalSize = size ?? Size(
mediaQuery.size.width * 0.8,
mediaQuery.size.height * 0.8,
);
// 默认居中显示
final modalPosition = position ?? Offset(
(mediaQuery.size.width - modalSize.width) / 2,
(mediaQuery.size.height - modalSize.height) / 2,
);
Offset currentPosition = modalPosition; // 模态窗口的当前坐标
_currentEntry = OverlayEntry(
builder: (context) {
return Stack(
children: [
// 半透明背景层(遮罩层)
Positioned.fill(
child: GestureDetector(
onTap: barrierDismissible ? () => dismiss() : null, // 控制点击遮罩层是否销毁该模态窗口
child: Container(color: barrierColor), // 背景色
),
),
// 模态窗口内容
Positioned(
left: currentPosition.dx,
top: currentPosition.dy,
child: Material(
elevation: 8,
borderRadius: BorderRadius.circular(12),
child: Container(
width: modalSize.width, // 尺寸
height: modalSize.height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
// 拖拽手柄区域
if (draggable)
GestureDetector(
onPanUpdate: (details) {
currentPosition += details.delta;
_currentEntry?.markNeedsBuild();
},
child: Container(
height: 40,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
),
),
child: const Center(
child: Icon(
Icons.drag_handle,
color: Colors.grey,
),
),
),
),
// 关闭按钮
const Align(
alignment: Alignment.topRight,
child: IconButton(
icon: Icon(Icons.close),
onPressed: dismiss,
),
),
// 内容区域
Expanded(
child: Padding(
padding: const EdgeInsets.all(16),
child: child, // 内容窗口
),
),
],
),
),
),
),
// // ),
],
);
},
);
overlayState.insert(_currentEntry!);
}
/// 关闭当前模态窗口
static void dismiss() {
if (_currentEntry != null) {
_currentEntry?.remove();
_currentEntry = null;
}
}
}
(2)使用示例
文件名 main.dart:
import 'package:flutter/material.dart';
import './global_manager.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Overlay 示例'),
),
body: const GlobalModalExample(),
),
);
}
}
class GlobalModalExample extends StatelessWidget {
const GlobalModalExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('全局模态窗口示例')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
child: const Text('基本模态窗口'),
onPressed: () => GlobalModal.show(
context: context,
child: const Center(
child: Text('这是一个基本模态窗口', style: TextStyle(fontSize: 20)),
),
),
),
const SizedBox(height: 20),
ElevatedButton(
child: const Text('自定义尺寸模态窗口'),
onPressed: () => GlobalModal.show(
context: context,
size: const Size(300, 400),
child: ListView.builder(
itemCount: 20,
itemBuilder: (_, index) => ListTile(
title: Text('项目 $index'),
),
),
),
),
const SizedBox(height: 20),
ElevatedButton(
child: const Text('可拖拽模态窗口'),
onPressed: () => GlobalModal.show(
context: context,
draggable: true,
child: const Column(
children: [
Text('可拖拽窗口', style: TextStyle(fontSize: 20)),
SizedBox(height: 20),
ElevatedButton(
onPressed: GlobalModal.dismiss,
child: Text('关闭'),
),
],
),
),
),
const SizedBox(height: 20),
ElevatedButton(
child: const Text('禁止点击外部关闭'),
onPressed: () => GlobalModal.show(
context: context,
barrierDismissible: false,
child: const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('必须点击按钮关闭', style: TextStyle(fontSize: 20)),
SizedBox(height: 20),
ElevatedButton(
onPressed: GlobalModal.dismiss,
child: Text('关闭窗口'),
),
],
),
),
),
),
],
),
),
);
}
}
参考:
Flutter悬浮UI的设计Overlay组件_flutter overlay-CSDN博客
Flutter的Overlay,你飘了。全局提示,悬浮按钮,弹出菜单在Flutter开发中,我们经常会遇到需要在当前页面 - 掘金

浙公网安备 33010602011771号