You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何移除Flutter可展开FAB右侧边距,使遮罩填满全屏?

解决Scaffold中FAB遮罩无法填满右侧视图的问题

我在实现可展开浮动操作按钮(FAB)时,点击FAB会显示深色半透明遮罩,但Scaffold自带右侧边距,导致遮罩没法铺满整个视图,怎么移除这个边距?

当前效果:
遮罩未填满右侧的效果


现有代码

Scaffold代码

return Scaffold(
        resizeToAvoidBottomInset: true,
        floatingActionButton: !isSearchBarVisible ? SizedBox.expand(
          child: ExpandableFab(
            //key: _key,
            distance: size.height * 0.09,
            children: [
              /* 无关代码已省略 */
            ],
          ),
        ) : null,
        body: /* 页面主体内容 */

ExpandableFab类代码

class ExpandableFab extends StatefulWidget {
  const ExpandableFab({
    Key? key,
    this.initialOpen,
    required this.distance,
    required this.children,
  }) : super(key: key);

  final bool? initialOpen;
  final double distance;
  final List<Widget> children;

  @override
  _ExpandableFabState createState() => _ExpandableFabState();
}

class _ExpandableFabState extends State<ExpandableFab>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  Animation<double>? _expandAnimation;
  bool _open = false;

  @override
  void initState() {
    super.initState();
    _open = widget.initialOpen ?? false;
    _controller = AnimationController(
      value: _open ? 1.0 : 0.0,
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    _expandAnimation = CurvedAnimation(
      curve: Curves.fastOutSlowIn,
      reverseCurve: Curves.easeOutQuad,
      parent: _controller,
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _toggle() {
    setState(() {
      _open = !_open;
      if (_open) {
        _controller.forward();
      } else {
        _controller.reverse();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    return GestureDetector(
      onTap: () => _toggle(),
      child: Stack(
        alignment: Alignment.bottomRight,
        clipBehavior: Clip.none,
        children: [
            IgnorePointer(
              ignoring: !_open,
              child: TweenAnimationBuilder<double>(
                tween: Tween<double>(begin: 0.0, end: _open ? 1.0 : 0.0),
                duration: Duration(milliseconds: 500),
                curve: Curves.easeInOut,
                builder: (_, value, child) {
                  if (value < 0.001) {
                    return child!;
                  }
                  return BackdropFilter(
                    filter: ImageFilter.blur(sigmaX: value, sigmaY: value),
                    child: child,
                  );
                },
                child: Container(color: Colors.transparent),
              ),
            ),
            IgnorePointer(
              ignoring: !_open,
              child: AnimatedOpacity(
                duration: Duration(milliseconds: 500),
                opacity: _open ? 1 : 0,
                curve: Curves.easeInOut,
                child: Container(
                  color: Colors.black12,
                ),
              ),
            ),
          Transform.translate(
            offset: Offset(0, 0),
            child: Stack(
              alignment: Alignment.bottomRight,
              children: [
                Positioned(
                    bottom: size.height * 0.14,
                    child: _buildTapToCloseFab(size)
                ),
                Positioned(
                    bottom: size.height * 0.14,
                    child: _buildTapToOpenFab(size)
                ),
                ..._buildExpandingActionButtons(),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildTapToCloseFab(Size size) {
    return SizedBox(
      width: 56.0,
      height: 56.0,
      child: Center(
        child: Material(
          shape: const CircleBorder(),
          clipBehavior: Clip.antiAlias,
          elevation: 4.0,
          child: InkWell(
            onTap: _toggle,
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: Icon(
                Icons.close,
                color: Theme.of(context).primaryColor,
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildTapToOpenFab(Size size) {
    return IgnorePointer(
      ignoring: _open,
      child: AnimatedContainer(
        transformAlignment: Alignment.center,
        transform: Matrix4.diagonal3Values(
          _open ? 0.7 : 1.0,
          _open ? 0.7 : 1.0,
          1.0,
        ),
        duration: const Duration(milliseconds: 250),
        curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
        child: AnimatedOpacity(
          opacity: _open ? 0.0 : 1.0,
          curve: const Interval(0.25, 1.0, curve: Curves.easeInOut),
          duration: const Duration(milliseconds: 250),
          child: FloatingActionButton(
            onPressed: _toggle,
            child: Icon(
                Icons.add,
            ),
          ),
        ),
      ),
    );
  }

  List<Widget> _buildExpandingActionButtons() {
    final children = <Widget>[];
    final count = widget.children.length;
    final step = 90.0 / (count - 1);
    var dist;
    for (var i = 0, angleInDegrees = 0.0;
        i < count;
        i++, angleInDegrees += step) {
      if (i == 0) {
        dist = (widget.distance) * (i + 1);
      }
      else {
        dist = (widget.distance) * (i + 1);
      }
      children.add(
        _ExpandingActionButton(
          directionInDegrees: 90,
          maxDistance: dist,
          progress: _expandAnimation,
          child: widget.children[i],
        ),
      );
    }
    return children;
  }
}

class _ExpandingActionButton extends StatelessWidget {
  const _ExpandingActionButton({
    Key? key,
    required this.directionInDegrees,
    required this.maxDistance,
    required this.progress,
    required this.child,
  }) : super(key: key);

  final double directionInDegrees;
  final double maxDistance;
  final Animation<double>? progress;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    return AnimatedBuilder(
      animation: progress!,
      builder: (BuildContext context, Widget? child) {
        final offset = Offset.fromDirection(
          directionInDegrees * (math.pi / 180.0),
          progress!.value * maxDistance,
        );
        return Positioned(
          right: 4.0 + offset.dx,
          bottom: (size.height * 0.14) + 4.0 + offset.dy,
          child: Transform.rotate(
            angle: (1.0 - progress!.value) * math.pi / 2,
            child: child,
          ),
        );
      },
      child: FadeTransition(
        opacity: progress!,
        child: child,
      ),
    );
  }
}

解决方案

问题根源是Scaffold的floatingActionButton参数默认会给子组件添加16dp的边距,即使套了SizedBox.expand也会被这个边距约束,导致遮罩无法铺满全屏。有两种可行的解决方式:

方式1:将遮罩与FAB分离,移到Scaffold的body层

把遮罩作为body的上层组件,脱离FAB区域的约束:

// 父组件中维护FAB的打开状态
bool _isFabOpen = false;

return Scaffold(
  resizeToAvoidBottomInset: true,
  floatingActionButton: !isSearchBarVisible ? ExpandableFab(
    distance: size.height * 0.09,
    children: [/* 子组件 */],
    onFabToggle: (isOpen) => setState(() => _isFabOpen = isOpen), // 新增回调传递状态
  ) : null,
  body: Stack(
    children: [
      /* 原有的body内容 */
      // 遮罩层
      if (_isFabOpen)
        GestureDetector(
          onTap: () => setState(() => _isFabOpen = false), // 点击遮罩关闭FAB
          child: Container(
            width: double.infinity,
            height: double.infinity,
            color: Colors.black12,
            child: BackdropFilter(
              filter: ImageFilter.blur(sigmaX: 1.0, sigmaY: 1.0),
              child: Container(color: Colors.transparent),
            ),
          ),
        ),
    ],
  ),
);

同时给ExpandableFab添加状态回调:

class ExpandableFab extends StatefulWidget {
  const ExpandableFab({
    Key? key,
    this.initialOpen,
    required this.distance,
    required this.children,
    required this.onFabToggle, // 新增回调
  }) : super(key: key);

  final bool? initialOpen;
  final double distance;
  final List<Widget> children;
  final Function(bool isOpen) onFabToggle;

  // ... 其余代码不变

  void _toggle() {
    setState(() {
      _open = !_open;
      widget.onFabToggle(_open); // 通知父组件状态变化
      if (_open) {
        _controller.forward();
      } else {
        _controller.reverse();
      }
    });
  }
}

方式2:将整个ExpandableFab移到body的Stack中

直接放弃使用Scaffold的floatingActionButton参数,把ExpandableFab作为body层Stack的子组件:

return Scaffold(
  resizeToAvoidBottomInset: true,
  body: Stack(
    children: [
      /* 原有的body内容 */
      if (!isSearchBarVisible)
        ExpandableFab(
          distance: size.height * 0.09,
          children: [/* 子组件 */],
        ),
    ],
  ),
);

这样ExpandableFab内部的遮罩就能自然铺满整个屏幕,不再受Scaffold的FAB边距限制。


内容的提问来源于stack exchange,提问作者Karol Wiśniewski

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 23:15:40