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

如何实现Flutter AppBar底部组件随滚动渐隐渐显(反向SliverAppBar效果)

实现反向滚动行为:固定AppBar,底部组件随滚动隐藏/显示

需求:与SliverAppBar搭配TabBar的默认行为相反——AppBar始终保持可见,底部的Chip栏(或TabBar)在向下滚动时逐渐消失,向上滚动时重新显示。

测试代码(原实现)

import 'package:flutter/material.dart';

void main() {
  runApp(const TestWidget());
}

class TestWidget extends StatelessWidget {
  const TestWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.light(
        useMaterial3: true,
      ),
      home: Scaffold(
        body: CustomScrollView(
          slivers: [
            SliverAppBar(
              floating: true,
              pinned: true,
              snap: true,
              title: const Text("My App Title"),
              bottom: PreferredSize(
                preferredSize: const Size.fromHeight(kToolbarHeight),
                child: SizedBox(
                  height: kToolbarHeight,
                  child: ListView.builder(
                    scrollDirection: Axis.horizontal,
                    itemCount: 25,
                    itemBuilder: (context, index) => FilterChip(
                      label: Text("Chip $index"),
                      onSelected: (_) {},
                    ),
                  ),
                ),
              ),
            ),
            SliverList(
              delegate: SliverChildListDelegate(
                List.generate(
                  100,
                  (index) => ListTile(
                    title: Text("Item $index"),
                  ),
                ),
              ),
            )
          ],
        ),
      ),
    );
  }
}

实现思路

原生SliverAppBar的bottom属性会和AppBar绑定,无法单独控制其显示隐藏,因此需要拆分组件并通过滚动监听+动画控制实现需求:

  • 将AppBar单独设置为固定顶部(SliverAppBar(pinned: true),移除bottom属性)
  • 把底部Chip栏做成独立组件,放入CustomScrollView的Sliver列表中
  • 通过NotificationListener监听滚动事件,判断滚动方向(向上/向下)
  • 根据滚动方向,用动画控制器控制Chip栏的垂直偏移,实现平滑的滑入/滑出效果

完整实现代码

import 'package:flutter/material.dart';

void main() {
  runApp(const TestWidget());
}

class TestWidget extends StatefulWidget {
  const TestWidget({super.key});

  @override
  State<TestWidget> createState() => _TestWidgetState();
}

class _TestWidgetState extends State<TestWidget> with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _offsetAnimation;
  double _previousScrollOffset = 0;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
    _offsetAnimation = Tween<double>(begin: 0, end: kToolbarHeight).animate(
      CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
    );
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _handleScroll(ScrollNotification notification) {
    if (notification is ScrollUpdateNotification) {
      final currentOffset = notification.metrics.pixels;
      // 向下滚动且超过AppBar高度时,隐藏底部栏
      if (currentOffset > _previousScrollOffset && currentOffset > kToolbarHeight) {
        if (_animationController.status != AnimationStatus.forward) {
          _animationController.forward();
        }
      } 
      // 向上滚动时,显示底部栏
      else if (currentOffset < _previousScrollOffset) {
        if (_animationController.status != AnimationStatus.reverse) {
          _animationController.reverse();
        }
      }
      _previousScrollOffset = currentOffset;
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.light(useMaterial3: true),
      home: Scaffold(
        body: NotificationListener<ScrollNotification>(
          onNotification: (notification) {
            _handleScroll(notification);
            return false;
          },
          child: CustomScrollView(
            slivers: [
              // 固定在顶部的AppBar
              const SliverAppBar(
                pinned: true,
                title: Text("My App Title"),
                floating: false,
                snap: false,
              ),
              // 可隐藏的Chip栏
              SliverToBoxAdapter(
                child: AnimatedBuilder(
                  animation: _offsetAnimation,
                  builder: (context, child) {
                    return Transform.translate(
                      offset: Offset(0, _offsetAnimation.value),
                      child: child,
                    );
                  },
                  child: SizedBox(
                    height: kToolbarHeight,
                    child: ListView.builder(
                      scrollDirection: Axis.horizontal,
                      itemCount: 25,
                      itemBuilder: (context, index) => FilterChip(
                        label: Text("Chip $index"),
                        onSelected: (_) {},
                      ),
                    ),
                  ),
                ),
              ),
              // 内容列表
              SliverList(
                delegate: SliverChildListDelegate(
                  List.generate(
                    100,
                    (index) => ListTile(title: Text("Item $index")),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

关键细节说明

  • 滚动触发优化:仅当滚动偏移超过AppBar高度时才触发隐藏,避免初始滚动时的误操作
  • 平滑动画:使用AnimationController和CurvedAnimation实现自然的滑入滑出效果,避免生硬的显示/隐藏
  • 状态管理:通过StatefulWidget维护动画控制器和滚动偏移量,确保状态正确更新

内容的提问来源于stack exchange,提问作者astubenbord

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 06:01:00