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

Flutter:上下滑动及列表滚动切换两种AppBar的实现求助

Flutter动态AppBar优化实现方案

问题背景

页面初始显示高高度的appbardown,用户向上滑动或在"Ones"列表滚动时切换为低高度的appbarup;向下滑动或列表滚动到顶部时切换回appbardown。现有实现过渡生硬低效,且列表滚动与AppBar切换的联动逻辑不顺畅。

优化方案

1. 用AnimatedContainer实现平滑高度过渡

放弃AnimatedSwitcher切换两个组件的方式,直接通过动画改变单个容器的高度,避免组件重建,提升流畅度。

2. 滚动监听与手势联动

给列表添加ScrollController,监听滚动偏移量:

  • 向上滚动时,先触发AppBar高度收缩动画,动画完成后再响应列表滚动
  • 向下滚动到列表顶部时,触发AppBar高度展开动画

3. 避免手势冲突

调整全局GestureDetector的行为,既保留页面滑动逻辑,又不干扰列表自身的滚动交互。

完整实现代码

import 'package:flutter/material.dart';

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

  @override
  State<TempScreen> createState() => _TempScreenState();
}

class _TempScreenState extends State<TempScreen> with TickerProviderStateMixin {
  final scrollController = ScrollController();
  late AnimationController _appBarAnimationController;
  late Animation<double> _appBarHeightAnimation;

  // AppBar高度比例参数
  final double _expandedHeightRatio = 0.29;
  final double _collapsedHeightRatio = 0.15;
  bool _isAppBarExpanded = true;
  bool _isAnimating = false;

  @override
  void initState() {
    super.initState();
    // 初始化动画控制器
    _appBarAnimationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );

    // 初始化高度动画
    _appBarHeightAnimation = Tween<double>(
      begin: _expandedHeightRatio,
      end: _collapsedHeightRatio,
    ).animate(CurvedAnimation(
      parent: _appBarAnimationController,
      curve: Curves.easeInOut,
    ));

    // 监听列表滚动
    scrollController.addListener(_handleScroll);
  }

  void _handleScroll() {
    if (_isAnimating) return;

    final direction = scrollController.position.userScrollDirection;

    if (direction == ScrollDirection.forward) {
      // 向下滚动到顶部时展开AppBar
      if (scrollController.offset <= 0 && !_isAppBarExpanded) {
        _expandAppBar();
      }
    } else if (direction == ScrollDirection.reverse) {
      // 向上滚动时收缩AppBar
      if (_isAppBarExpanded) {
        _collapseAppBar();
      }
    }
  }

  // 收缩AppBar
  void _collapseAppBar() {
    if (_isAnimating) return;
    _isAnimating = true;
    _appBarAnimationController.forward().then((_) {
      setState(() {
        _isAppBarExpanded = false;
        _isAnimating = false;
      });
    });
  }

  // 展开AppBar
  void _expandAppBar() {
    if (_isAnimating) return;
    _isAnimating = true;
    _appBarAnimationController.reverse().then((_) {
      setState(() {
        _isAppBarExpanded = true;
        _isAnimating = false;
      });
    });
  }

  // 处理页面滑动手势
  void _handleVerticalDragUpdate(DragUpdateDetails details) {
    if (_isAnimating) return;

    if (details.delta.dy < -5 && _isAppBarExpanded) {
      // 向上滑动超过阈值,收缩AppBar
      _collapseAppBar();
    } else if (details.delta.dy > 5 && !_isAppBarExpanded) {
      // 向下滑动超过阈值,展开AppBar
      _expandAppBar();
    }
  }

  @override
  void dispose() {
    _appBarAnimationController.dispose();
    scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    Size size = MediaQuery.of(context).size;
    return Scaffold(
      body: GestureDetector(
        onVerticalDragUpdate: _handleVerticalDragUpdate,
        behavior: HitTestBehavior.translucent,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 动态高度的AppBar
            AnimatedBuilder(
              animation: _appBarHeightAnimation,
              builder: (context, child) {
                return Container(
                  width: double.infinity,
                  height: size.height * _appBarHeightAnimation.value,
                  decoration: BoxDecoration(
                    color: Theme.of(context).primaryColor,
                    borderRadius: const BorderRadius.only(
                      bottomLeft: Radius.circular(20),
                      bottomRight: Radius.circular(20),
                    ),
                  ),
                );
              },
            ),
            Padding(
              padding: EdgeInsets.only(
                  left: size.width * 0.05, top: size.height * 0.035),
              child: const Text(
                'Quick Links',
                style: TextStyle(
                  fontSize: 20.0,
                  color: Colors.black,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
            SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              child: Padding(
                padding: EdgeInsets.only(left: size.width * 0.05),
                child: const Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Text('A'),
                    Text('B'),
                    Text('C'),
                    Text('D'),
                    Text('E'),
                  ],
                ),
              ),
            ),
            Expanded(
              child: Padding(
                padding: EdgeInsets.only(
                  top: 20,
                  left: size.width * 0.04,
                  right: size.width * 0.04,
                ),
                child: DefaultTabController(
                  length: 3,
                  child: Column(
                    children: [
                      Padding(
                        padding: EdgeInsets.only(
                          left: size.width * 0.04,
                          right: size.width * 0.04,
                        ),
                        child: const TabBar(
                          indicatorWeight: 4.0,
                          indicatorColor: Colors.black,
                          labelStyle: TextStyle(
                            fontSize: 18.0,
                            fontWeight: FontWeight.w700,
                          ),
                          tabs: [
                            Tab(text: '1'),
                            Tab(text: '2'),
                            Tab(text: '3'),
                          ],
                          unselectedLabelColor: Color.fromARGB(255, 186, 186, 186),
                        ),
                      ),
                      Expanded(
                        child: TabBarView(
                          children: [
                            One(scrollController: scrollController),
                            const Two(),
                            const Three(),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class One extends StatelessWidget {
  const One({super.key, required this.scrollController});

  final ScrollController scrollController;

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      controller: scrollController,
      padding: const EdgeInsets.only(top: 10),
      child: const Column(
        children: [
          Text('one', style: TextStyle(fontSize: 50)),
          Text('one', style: TextStyle(fontSize: 50)),
          Text('one', style: TextStyle(fontSize: 50)),
          Text('one', style: TextStyle(fontSize: 50)),
          Text('one', style: TextStyle(fontSize: 50)),
          Text('one', style: TextStyle(fontSize: 50)),
          Text('one', style: TextStyle(fontSize: 50)),
          Text('one', style: TextStyle(fontSize: 50)),
        ],
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return const Center(child: Text('Two'));
  }
}

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

  @override
  Widget build(BuildContext context) {
    return const Center(child: Text('Three'));
  }
}

关键优化点说明

  • 动画流畅性:使用AnimatedBuilder和AnimationController实现高度的平滑过渡,避免组件重建带来的性能损耗
  • 滚动联动:通过ScrollController监听列表滚动状态,精准控制AppBar的展开/收缩时机,实现先完成AppBar过渡再允许列表滚动的逻辑
  • 手势处理:GestureDetector设置HitTestBehavior.translucent,既处理页面整体滑动,又不会拦截列表的滚动手势
  • 状态锁:添加_isAnimating状态锁,防止动画过程中重复触发手势事件,避免动画混乱

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 07:40:55