Flutter 自定义居中弹框:智能上移 + 遮罩 + 动画(附
2025-12-10 本文已影响0人
那年那月那花儿
在移动端产品中,弹框是非常常见的组件。
但 Flutter 默认的 showDialog 存在一些不足:
• 无法智能上移(键盘弹出会遮挡弹框)
• 难以自定义动画
• 难以完全控制内容布局与交互
• 默认黑色遮罩不够灵活
因此,我封装了一个 可复用、可定制、动画柔和、支持键盘智能上移的居中弹框组件 —— PopupCenterPanel。
本文将详细介绍其实现原理,并提供完整、可直接使用的代码。
🚀 一、效果展示
✔ 居中弹出
✔ 键盘弹出时自动上移
✔ 自定义内容
✔ 自定义遮罩、点击关闭
✔ 支持自定义动画
这种效果在登录验证码输入框、表单弹窗、密码修改等场景非常常见。
import 'package:flutter/material.dart';
/// 自定义居中弹框(智能上移、遮罩、动画)
class PopupCenterPanel {
static Future<T?> show<T>({
required BuildContext context,
required Widget child,
bool barrierDismissible = true,
Color barrierColor = const Color.fromRGBO(0, 0, 0, 0.45),
}) {
return showGeneralDialog(
context: context,
barrierDismissible: barrierDismissible,
barrierLabel: "PopupCenterPanel",
barrierColor: barrierColor,
transitionDuration: const Duration(milliseconds: 200),
pageBuilder: (_, __, ___) => const SizedBox.shrink(),
transitionBuilder: (_, animation, __, ___) {
return _PopupBody(animation: animation, child: child);
},
);
}
}
/// 内部包装(智能上移 + 动画)
class _PopupBody extends StatefulWidget {
final Animation<double> animation;
final Widget child;
const _PopupBody({required this.animation, required this.child});
@override
State<_PopupBody> createState() => _PopupBodyState();
}
class _PopupBodyState extends State<_PopupBody> {
final GlobalKey _key = GlobalKey();
double _offset = 0;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateOffset();
}
@override
void didUpdateWidget(covariant _PopupBody oldWidget) {
super.didUpdateWidget(oldWidget);
_updateOffset();
}
void _updateOffset() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final box = _key.currentContext?.findRenderObject() as RenderBox?;
if (box == null) return;
final screenHeight = MediaQuery.of(context).size.height;
final keyboard = MediaQuery.of(context).viewInsets.bottom;
final modalHeight = box.size.height;
final modalBottom = screenHeight / 2 + modalHeight / 2;
final distanceToBottom = screenHeight - modalBottom;
if (keyboard > distanceToBottom) {
// 需要上移
final needMove = keyboard - distanceToBottom + 16;
if (_offset != needMove) setState(() => _offset = needMove);
} else {
if (_offset != 0) setState(() => _offset = 0);
}
});
}
@override
Widget build(BuildContext context) {
final anim = CurvedAnimation(parent: widget.animation, curve: Curves.easeOut);
return FadeTransition(
opacity: anim,
child: ScaleTransition(
scale: Tween(begin: 0.95, end: 1.0).animate(anim),
child: Center(
child: Transform.translate(
offset: Offset(0, -_offset),
child: Material(
key: _key,
borderRadius: BorderRadius.circular(16),
color: Colors.white,
child: widget.child,
),
),
),
),
);
}
}
二、使用方式
示例:调用弹框显示一个验证码输入框。
PopupCenterPanel.show(
context: context,
barrierDismissible: false,
child: SizedBox(
width: 300,
child: Padding(
padding: const EdgeInsets.all(20),
child: Text("这是你的弹框内容"),
),
),
);