Flutter点击FAB实现下到上动画切页、FAB旋转且底部导航固定如何实现?
Flutter FAB联动底部弹出页实现方案
核心逻辑:要保证底部导航栏无位移,不使用系统路由跳转逻辑,而是在Scaffold的Body层叠放置弹出页容器,通过自定义动画控制显隐,BottomNavigationBar始终固定在Scaffold层级不受影响。
依赖变量与动画初始化
在State类中定义以下变量,在生命周期中完成动画配置:
import 'package:flutter/material.dart'; import 'dart:math' as math; class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin { // 弹出页开关状态 bool _isPanelOpen = false; // 动画控制器 late AnimationController _animationController; // FAB旋转动画 late Animation<double> _fabRotateAnim; // 弹出位移动画 late Animation<Offset> _panelSlideAnim; // 背景遮罩透明度动画 late Animation<double> _maskOpacityAnim; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 300), ); // FAB从0度转到45度 _fabRotateAnim = Tween<double>(begin: 0, end: math.pi / 4).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), ); // 弹出页从屏幕外底部滑入 _panelSlideAnim = Tween<Offset>(begin: const Offset(0, 1), end: Offset.zero).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), ); // 背景遮罩从透明到半黑 _maskOpacityAnim = Tween<double>(begin: 0, end: 0.5).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), ); } @override void dispose() { _animationController.dispose(); super.dispose(); } // FAB点击触发逻辑 void _togglePanel() { if (_isPanelOpen) { _animationController.reverse(); } else { _animationController.forward(); } setState(() { _isPanelOpen = !_isPanelOpen; }); }
页面结构实现
Scaffold结构中BottomNavigationBar正常配置,Body用Stack层叠原有页面、遮罩、弹出页三层:
@override Widget build(BuildContext context) { return Scaffold( // 底部导航栏固定在Scaffold层级,不会受弹出页动画影响 bottomNavigationBar: BottomNavigationBar( items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"), BottomNavigationBarItem(icon: Icon(Icons.person), label: "我的"), ], ), floatingActionButton: AnimatedBuilder( animation: _fabRotateAnim, builder: (context, child) => Transform.rotate( angle: _fabRotateAnim.value, child: FloatingActionButton( onPressed: _togglePanel, child: const Icon(Icons.add), ), ), ), body: Stack( children: [ // 第一层:原有页面内容 const Center(child: Text("原有页面内容")), // 第二层:半透明遮罩,点击也可关闭弹出页 AnimatedBuilder( animation: _maskOpacityAnim, builder: (context, child) => _maskOpacityAnim.value > 0 ? GestureDetector( onTap: _togglePanel, child: Container( width: double.infinity, height: double.infinity, color: Colors.black.withOpacity(_maskOpacityAnim.value), ), ) : const SizedBox.shrink(), ), // 第三层:底部弹出页 AnimatedBuilder( animation: _panelSlideAnim, builder: (context, child) => Transform.translate( offset: _panelSlideAnim.value, child: Align( alignment: Alignment.bottomCenter, child: Container( width: double.infinity, height: MediaQuery.of(context).size.height * 0.75, color: Colors.white, // 此处可替换为你需要的弹出页内容 child: const Center(child: Text("弹出页内容")), ), ), ), ) ], ), ); } }
效果验证
- FAB默认显示+号图标,点击后平滑旋转45度变成×号样式,再次点击恢复原状
- 弹出页从底部平滑滑入,滑出过程中底部导航栏全程保持原位无位移
- 点击半透明遮罩也可触发关闭逻辑,可根据需求自行删减该交互
内容的提问来源于stack exchange,提问作者krunal
相关产品推荐
相关产品推荐

