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("这是你的弹框内容"),
    ),
  ),
);
上一篇 下一篇

猜你喜欢

热点阅读