Flutter Web如何正确实现侧边栏双状态平滑过渡动画?
问题原因
AnimatedPositioned生效的前置要求是作为Stack组件的直接子元素,原代码未将其放置在Stack中,组件无法识别定位变化的补间需求,直接触发布局重绘- 原代码同时配置
left、right两个方向的对立定位值,状态切换时两个参数同时跳变,Flutter无法计算连续的中间布局状态,直接渲染最终位置 - 侧边栏主体采用两个独立Container做展开/收起的分支渲染,组件宽度没有过渡过程,即使位置动画生效,内容区也会直接跳变
- 已初始化的
AnimationController仅被用来判断动画完成状态,没有参与动画值的计算,没有发挥补间动画的控制能力
关键修改说明
- 强制要求
AnimatedPositioned作为Stack的直接子组件,这是定位类动画组件生效的必要前提 - 移除同时设置
left、right跳变值的逻辑,固定垂直方向top、bottom为0撑满高度,仅通过left值的连续变化实现侧边栏滑入滑出 - 用
AnimatedContainer替代两个条件分支渲染的Container,实现侧边栏宽度从收起值到展开值的平滑过渡,避免内容跳变 - 抽离固定尺寸常量,减少魔法值,避免布局计算冲突
修复后完整代码
class _SideNavigationBarState extends State<SideNavigationBar> with SingleTickerProviderStateMixin<SideNavigationBar> { late AnimationController _animationController; late StreamController<bool> isSideNavigationBarOpenedStreamController; late Stream<bool> isSideNavigationBarOpenedStream; late StreamSink<bool> isSideNavigationBarOpenedSink; final _animationDuration = const Duration(milliseconds: 500); // 侧边栏固定尺寸常量 final double _collapseWidth = 45; final double _expandWidth = 300; @override void initState() { super.initState(); _animationController = AnimationController(vsync: this, duration: _animationDuration); isSideNavigationBarOpenedStreamController = PublishSubject<bool>(); isSideNavigationBarOpenedStream = isSideNavigationBarOpenedStreamController.stream; isSideNavigationBarOpenedSink = isSideNavigationBarOpenedStreamController.sink; } @override void dispose() { _animationController.dispose(); isSideNavigationBarOpenedStreamController.close(); isSideNavigationBarOpenedSink.close(); super.dispose(); } void onIconPressed() { final animationStatus = _animationController.status; final isAnimationCompleted = animationStatus == AnimationStatus.completed; if (isAnimationCompleted) { isSideNavigationBarOpenedSink.add(false); _animationController.reverse(); } else { isSideNavigationBarOpenedSink.add(true); _animationController.forward(); } } @override Widget build(BuildContext context) { double triggerBtnSize = 52; final screenWidth = MediaQuery.of(context).size.width; return StreamBuilder<bool>( initialData: false, stream: isSideNavigationBarOpenedStream, builder: (context, isSideNavigationBarOpenedAsync) { final isOpened = isSideNavigationBarOpenedAsync.data!; final triggerIcon = isOpened ? Icons.arrow_back_ios : Icons.arrow_forward_ios; return AnimatedPositioned( duration: _animationDuration, top: 0, bottom: 0, left: isOpened ? 0 : -(_expandWidth - _collapseWidth), child: Row( children: <Widget>[ AnimatedContainer( duration: _animationDuration, width: isOpened ? _expandWidth : _collapseWidth, color: Colors.grey, ), Align( alignment: const Alignment(0, -0.9), child: GestureDetector( onTap: onIconPressed, child: SizedBox( width: triggerBtnSize, height: triggerBtnSize, child: Material( color: Colors.transparent, child: InkWell( child: Icon( triggerIcon, color: Colors.grey.withOpacity(0.5), ), ), ), ), ), ) ], ), ); }, ); } }
组件使用注意
侧边栏必须放在Stack组件内才能正常工作,使用示例:
@override Widget build(BuildContext context) { return Scaffold( body: Stack( children: [ // 页面主内容区 const Positioned.fill( child: MainPageContent(), ), // 侧边栏作为Stack的直接子组件 const SideNavigationBar(), ], ), ); }
内容的提问来源于stack exchange,提问作者Eyal D
相关产品推荐
相关产品推荐

