如何移除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
相关产品推荐
相关产品推荐

