Flutter自定义Animated Container实现折叠工具栏效果求助

核心实现思路
你现在的代码核心问题是自定义的工具栏放在Scaffold的固定appBar属性里,无法跟随滚动折叠,需要结合SliverPersistentHeader和滚动监听实现联动效果,具体修改步骤如下:
- 移除Scaffold的appBar配置,将自定义工具栏逻辑迁移到NestedScrollView的headerSliverBuilder中,用SliverPersistentHeader包裹实现折叠能力
- 新增ScrollController监听页面滚动偏移,根据偏移量动态计算工具栏的高度、元素透明度等动画参数
- 优化动画配置,将原有AnimationController时长调整为300ms匹配交互体验,标签切换时同步触发折叠/展开动画
- 折叠过程中动态调整标题大小、顶部图标位置、标签栏的固定位置,实现连贯的过渡效果
核心修改代码片段
class _WatchState extends State<Watching> with TickerProviderStateMixin { late AnimationController _controller; late Animation<double> animation, animation2; final double maxAppBarHeight = 250; // 展开最大高度 final double minAppBarHeight = 80; // 折叠最小高度 late ScrollController _scrollController; // 新增滚动控制器 double scrollOffset = 0; List pages = []; int index = 0; GlobalKey _bottomNavigationKey = GlobalKey(); List titles = ["feed", "trending", "explore"]; @override void initState() { super.initState(); _scrollController = ScrollController(); _scrollController.addListener(() { setState(() { scrollOffset = _scrollController.offset; }); }); pages = [ MainPage( set_state: widget.set_state, scrollController: _scrollController, // 把控制器传给子页面绑定滚动 ), TrendingPage(set_state: widget.set_state, scrollController: _scrollController), ExplorePage(scrollController: _scrollController) ]; _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300)); animation = Tween<double>(begin: 125, end: 150).animate(_controller); animation2 = Tween<double>(begin: 125, end: 150).animate(_controller); } // 自定义SliverPersistentHeader代理 SliverPersistentHeader makeHeader() { return SliverPersistentHeader( pinned: true, // 折叠后标签栏固定在顶部 delegate: _SliverAppBarDelegate( minHeight: minAppBarHeight, maxHeight: maxAppBarHeight, child: Container( color: Colors.teal, child: Stack( children: [ // 根据滚动偏移计算高度比例 AnimatedContainer( duration: const Duration(milliseconds: 200), width: animation.value, height: maxAppBarHeight - scrollOffset < minAppBarHeight ? minAppBarHeight : maxAppBarHeight - scrollOffset, decoration: const BoxDecoration( color: Colors.teal, ), ), // 其余原有布局元素,根据滚动偏移调整位置和透明度 Align( alignment: Alignment.bottomRight, child: RotationTransition( turns: const AlwaysStoppedAnimation(0 / 360), child: ClipPath( clipper: LeftRoundedClipper(flip: true), child: AnimatedContainer( duration: const Duration(milliseconds: 200), width: animation2.value - 10, height: animation2.value * MediaQuery.of(context).size.height, decoration: BoxDecoration( color: Colors.teal, gradient: LinearGradient( colors: [ Colors.lightGreen.withGreen(10), Colors.greenAccent, Colors.greenAccent ], )), ), ), ), ), // 标签栏保持在底部,折叠后固定在顶部 Align( alignment: scrollOffset > maxAppBarHeight - minAppBarHeight ? Alignment.center : Alignment.bottomCenter, child: CurvedNavigationBar( // 原有标签栏配置不变 key: _bottomNavigationKey, height: 65, color: Colors.white, buttonBackgroundColor: Colors.transparent, backgroundColor: Colors.transparent, animationCurve: Curves.easeInOut, animationDuration: const Duration(milliseconds: 600), index: index, onTap: (value) { // 原有点击逻辑不变,补充控制器重置 _controller.reset(); switch (value) { case 0: animation2 = Tween<double>(begin: 125, end: 500) .animate(_controller); break; case 1: animation2 = Tween<double>(begin: 125, end: 700) .animate(_controller); break; case 2: animation = Tween<double>(begin: 125, end: 500) .animate(_controller); animation2 = Tween<double>(begin: 150, end: 200) .animate(_controller); break; } _controller.forward(); setState(() { index = value; }); }, items: [ Text( "Main", style: index == 0 ? const TextStyle( color: Colors.white, fontWeight: FontWeight.bold) : const TextStyle(color: Colors.black54), ), Text( "trending", style: index == 1 ? const TextStyle( color: Colors.white, fontWeight: FontWeight.bold) : const TextStyle(color: Colors.black54), ), Text( "explore", style: index == 2 ? const TextStyle( color: Colors.white, fontWeight: FontWeight.bold) : const TextStyle(color: Colors.black54), ), ], ), ), // 标题根据滚动偏移调整大小和位置 Align( alignment: scrollOffset > maxAppBarHeight - minAppBarHeight ? Alignment.centerLeft : Alignment.bottomLeft, child: Padding( padding: scrollOffset > maxAppBarHeight - minAppBarHeight ? const EdgeInsets.only(left: 80) : const EdgeInsets.only(bottom: 60, left: 10), child: Text( titles[index], style: TextStyle( fontSize: scrollOffset > maxAppBarHeight - minAppBarHeight ? 20 : 50, fontWeight: FontWeight.bold, color: Colors.white), ), )), // 顶部图标保持可见 Align( alignment: Alignment.topLeft, child: Padding( padding: const EdgeInsets.only(top: 15, left: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ index == 0 ? ShowIcon( iconName: 'assets/icons/User.png', onIconTap: () {}, ) : const SizedBox(), Padding( padding: const EdgeInsets.symmetric(horizontal: 25), child: Row( children: [ Padding( padding: const EdgeInsets.only(right: 14), child: ShowIcon( iconName: 'assets/icons/MagnifyingGlass.png', onIconTap: () {}, ), ), Padding( padding: const EdgeInsets.only(right: 14), child: ShowIcon( iconName: 'assets/icons/BellRinging.png', onIconTap: () {}, ), ), ShowIcon( iconName: 'assets/icons/PaperPlane.png', onIconTap: () {}, ), ], ), ) ], ), ), ) ], ), ), ), ); } @override Widget build(BuildContext context) { return SafeArea( child: Scaffold( backgroundColor: Colors.white, // 移除原有appBar配置 body: DefaultTabController( length: pages.length, child: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return <Widget>[ makeHeader(), // 把自定义工具栏放到Sliver列表里 ]; }, body: pages[index], ), ), ), ); } @override void dispose() { _controller.dispose(); _scrollController.dispose(); super.dispose(); } } // SliverPersistentHeader代理实现 class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { _SliverAppBarDelegate({ required this.minHeight, required this.maxHeight, required this.child, }); final double minHeight; final double maxHeight; final Widget child; @override double get minExtent => minHeight; @override double get maxExtent => maxHeight; @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { return SizedBox.expand(child: child); } @override bool shouldRebuild(_SliverAppBarDelegate oldDelegate) { return maxHeight != oldDelegate.maxHeight || minHeight != oldDelegate.minHeight || child != oldDelegate.child; } }
注意事项
- 所有子页面的滚动组件需要绑定传入的ScrollController,否则无法触发滚动监听
- 可根据实际需求调整maxAppBarHeight、minAppBarHeight的数值匹配设计稿
- 如果需要更精细的过渡效果,可将滚动偏移量归一化处理(offset/(maxHeight-minHeight)得到0-1的进度值),用该值控制所有元素的动画参数
内容的提问来源于stack exchange,提问作者Fire base
相关产品推荐
相关产品推荐

