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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.04 07:54:04