SliverAppBar的功能和用法


我们在上一章回中介绍了SliverGrid组件相关的内容,本章回中将介绍SliverAppBar组件.闲话休提,让我们一起Talk Flutter吧。

在这里插入图片描述

1 概念介绍

我们在本章回中介绍的SliverAppBar和普通的AppBar类似,它们的不同之处在于SliverAppBar创建的内容可以折叠和展开,因此它主要和SliverList等组件配合
使用,这样就可以创建一个滑动时折叠或者展开的标题,这样可以丰富标题的内容。本章回中将详细介绍SliverAppBar的使用方法。

2 使用方法

和其它组件一样SliverAppBar提供了相关的属性来控制自己,下面是常用的属性,掌握这些属性就可以使用SliverAppBar了。

  • title属性:主要用来显示标题,和普通AppBar中的title一样;
  • backgroundColor属性:主要用来控制appBar的背景颜色;
  • collapsedHeight属性:主要用来控制AppBar关闭时的高度;
  • expandedHeight属性:主要用来控制AppBar展开时的高度;
  • flexibleSpace属性:主要用来存放AppBar展开时的内容;
    上面介绍的这些属性中我重点介绍一下flexibleSpace属性,该属性是Widget类型,因此我们需要使用组件给它赋值,常用FlexibleSpace类型的组件给它赋值,该
    组件属于容器类组件,它本身只负责装饰,而不显示具体的内容,具体的内容在background属性对应的组件中。我们将在后面的小节中通过代码来演示它的用法。

3 示例代码

SliverAppBar(
  title: const Text('Title of SliverAppBar'),
  backgroundColor: Colors.purpleAccent,
  ///关闭和展开时的高度
  collapsedHeight: 60,
  expandedHeight: 300,
  ///下滑屏幕时先显示appBar下面的内容,后显示appBar中的内容,默认值为false表示此情况
  ///设置为true时,下滑屏幕时先显示appBar中的内容,后显示appBar下面的内容;
  floating: true,
  ///向上拖动屏幕,下面的内容向上滚动,appBar逐渐缩小,最后是否显示appBar,默认是56高度的appBar
  ///默认值为false,表示不显示
  pinned: true,

  ///appBar空间扩展后显示的内容
  flexibleSpace: FlexibleSpaceBar(
    ///这个title在appBar的最下方,可以不设定,缩小后它会和SliverAppBar的title重合
    title: const Text('title of FlexibleSpaceBar'),
    background: Container(
      decoration: const BoxDecoration(
        image:DecorationImage(
          opacity: 0.8,
          // colorFilter: ColorFilter.mode(Color.fromARGB(100, 200, 20,30),BlendMode.difference),
          image: AssetImage("images/ex.png"),
          fit: BoxFit.fill,
        ),
      ),
      ///扩展空间中主要显示的内容
      child: const Center(child: Text('child of container')),
    ),
    centerTitle: true,
    ///拉伸和折叠时的样式,效果不是很明显
    collapseMode: CollapseMode.pin,
    stretchModes: const [
      StretchMode.zoomBackground,
      StretchMode.blurBackground,
      StretchMode.fadeTitle,
    ],
  ),
),

上面的示例代码中添加了详细的注释,这样方便大家理解代码。不过依据目前的知识我们还不能演示程序的运行结果,因为还需要其它组件配合才可以运行,大家不用担心,目前只需要熟练掌握如何创建SliverAppBar组件就可以了,我们在后面章回中会使用本章回创建的组件给大家演示程序运行效果。

看官们,与"SliverAppBar组件"相关的内容就介绍到这里,欢迎大家在评论区交流与讨论!

<think>我们之前已经实现了字母导航栏的优化,现在用户希望集成吸顶功能。根据引用[1],吸顶功能可以通过SliverAppBarSliverPersistentHeader实现。具体来说: 1. 第一级吸顶使用SliverAppBar(用于头部Tab栏) 2. 第二级及以下使用SliverPersistentHeader(自定义delegate)实现任意组件的吸顶 用户需求:在字母导航列表的基础上,实现两级吸顶: 第一级:AppBar(标题栏)Tab栏(可能用户希望是字母导航的标题?或者额外的Tab?) 第二级:每个字母分组的标题(即字母标题)吸顶 但注意,用户之前的代码中,每个字母分组已经有一个标题(header),现在希望这些标题在滚动到顶部时吸顶。 根据引用[1]的示例,我们可以这样调整: 1. 将整个CustomScrollView的Slivers列表重新组织,使用SliverAppBar作为第一级吸顶(如果需要的话,但这里用户可能只需要一个固定的AppBar,所以我们可以考虑在Scaffold的appBar属性中设置,这样就已经吸顶了) 2. 对于每个字母分组的标题,我们使用SliverPersistentHeader来包裹,这样每个字母标题都可以吸顶(当滚动到该字母分组时,标题会吸顶直到被下一个字母标题推走) 具体修改步骤: 步骤1:将原来每个字母分组的SliverList拆分成两部分: - 字母标题(Header)部分:用SliverPersistentHeader包裹,并设置pinned: true(这样在离开时还会停留在顶部直到下一个吸顶头将它推走) - 列表项部分:仍然用SliverList 步骤2:修改_buildSliverSection方法,使其返回两个Sliver:一个SliverPersistentHeader(用于字母标题)一个SliverList(用于该字母下的列表项)。 注意:由于我们之前已经预计算了位置,但现在每个字母分组被拆分成了两个Sliver,预计算的位置需要调整吗? - 预计算的位置是每个字母分组的起始位置,这个起始位置就是字母标题的位置。所以预计算仍然有效,因为跳转时我们跳转到字母标题的位置(即SliverPersistentHeader的起始位置)。 步骤3:定义字母标题的SliverPersistentHeaderDelegate。 由于每个字母标题高度固定(_headerHeight),我们可以创建一个自定义的Delegate。 代码调整如下: 1. 创建自定义的SliverPersistentHeaderDelegate: ```dart class _AlphabetHeaderDelegate extends SliverPersistentHeaderDelegate { final Widget child; final double height; _AlphabetHeaderDelegate({required this.child, required this.height}); @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { return SizedBox.expand(child: child); } @override double get maxExtent => height; @override double get minExtent => height; @override bool shouldRebuild(covariant _AlphabetHeaderDelegate oldDelegate) { return oldDelegate.child != child || oldDelegate.height != height; } } ``` 2. 修改_buildSliverSection方法,使其返回两个Sliver(SliverPersistentHeaderSliverList): ```dart List<Widget> _buildSliverSection(String letter) { final items = _data[letter]!; return [ // 字母标题 - 吸顶Header SliverPersistentHeader( pinned: true, // 设置吸顶 delegate: _AlphabetHeaderDelegate( height: _headerHeight, child: Container( key: Key('header_$letter'), height: _headerHeight, color: Colors.blue[100], padding: const EdgeInsets.symmetric(horizontal: 16), alignment: Alignment.centerLeft, child: Text( letter, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ), ), ), // 列表项 SliverList( delegate: SliverChildBuilderDelegate( (context, index) { return SizedBox( height: _itemHeight, child: ListTile( title: Text(items[index]), leading: CircleAvatar( child: Text(letter), ), ), ); }, childCount: items.length, ), ), ]; } ``` 3. 修改Slivers的生成方式(因为现在每个字母分组返回两个Sliver): ```dart CustomScrollView( controller: _scrollController, cacheExtent: 1000, slivers: [ // 将每个字母分组生成的Sliver列表展开 ..._letters.expand((letter) { if (_data.containsKey(letter)) { return _buildSliverSection(letter); } return <Widget>[]; }).toList(), ], ) ``` 4. 注意:预计算位置的方法不需要改变,因为每个字母分组的位置仍然是其标题(现在在SliverPersistentHeader)的位置,而跳转时跳转到这个位置,标题就会在顶部(吸顶效果自然出现)。 5. 另外,我们可能希望整个页面的顶部有一个AppBar(已经在Scaffold中设置了),所以不需要再在Slivers中添加SliverAppBar(除非需要折叠效果)。如果不需要折叠,那么使用Scaffold的appBar即可。 6. 如果用户希望有类似引用[1]中的TabBar吸顶(在AppBar下方),那么我们可以考虑在Slivers的最前面添加一个SliverAppBar(作为第一级吸顶)一个TabBar(作为第二级吸顶)。但当前需求是字母标题吸顶,所以上述实现已经满足。 完整代码调整如下(只列出修改部分):</think># Flutter字母导航栏与吸顶功能集成实现 我将为您提供完整的字母导航栏与吸顶功能集成的解决方案,结合了之前实现的字母导航优化引用[1]中的吸顶技术。 ## 完整实现代码 ```dart import 'package:flutter/material.dart'; void main() => runApp(const AlphabetListApp()); class AlphabetListApp extends StatelessWidget { const AlphabetListApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: '字母导航+吸顶效果', theme: ThemeData(primarySwatch: Colors.blue), home: const AlphabetListWithSticky(), ); } } class AlphabetListWithSticky extends StatefulWidget { const AlphabetListWithSticky({super.key}); @override State<AlphabetListWithSticky> createState() => _AlphabetListWithStickyState(); } class _AlphabetListWithStickyState extends State<AlphabetListWithSticky> { final ScrollController _scrollController = ScrollController(); final Map<String, double> _sectionPositions = {}; final double _itemHeight = 60.0; final double _headerHeight = 40.0; final List<String> _letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); // 当前吸顶的字母 String _currentStickyLetter = ''; // 生成模拟数据 Map<String, List<String>> generateData() { final Map<String, List<String>> data = {}; for (final letter in _letters) { data[letter] = List.generate( (15 + letter.codeUnitAt(0) % 10), (i) => '$letter Item ${i + 1}' ); } return data; } final Map<String, List<String>> _data = {}; @override void initState() { super.initState(); _data.addAll(generateData()); // 监听滚动位置 _scrollController.addListener(_handleScroll); // 预计算位置 WidgetsBinding.instance.addPostFrameCallback((_) { _precalculatePositions(); }); } @override void dispose() { _scrollController.removeListener(_handleScroll); _scrollController.dispose(); super.dispose(); } // 预计算每个字母组的位置 void _precalculatePositions() { double position = 0; for (final letter in _letters) { if (_data.containsKey(letter)) { _sectionPositions[letter] = position; position += _headerHeight + (_data[letter]!.length * _itemHeight); } } } // 跳转到指定字母 void _jumpToSection(String letter) { final position = _sectionPositions[letter]; if (position != null) { _scrollController.jumpTo(position); } } // 处理滚动事件 void _handleScroll() { final double scrollPosition = _scrollController.position.pixels; String? newStickyLetter; // 查找当前应该吸顶的字母 for (final letter in _letters.reversed) { final position = _sectionPositions[letter]; if (position != null && scrollPosition >= position - _headerHeight) { newStickyLetter = letter; break; } } if (newStickyLetter != _currentStickyLetter) { setState(() { _currentStickyLetter = newStickyLetter ?? ''; }); } } @override Widget build(BuildContext context) { return Scaffold( body: CustomScrollView( controller: _scrollController, cacheExtent: 1000, slivers: [ // 一级吸顶:应用标题栏 [^1] const SliverAppBar( pinned: true, expandedHeight: 150, flexibleSpace: FlexibleSpaceBar( title: Text('字母导航+吸顶效果'), background: ColoredBox( color: Colors.blue, child: Center(child: Icon(Icons.sort_by_alpha, size: 60, color: Colors.white)), ), ), ), // 二级吸顶:字母导航标签栏 [^1] SliverPersistentHeader( pinned: true, delegate: _StickyTabBarDelegate( child: Container( color: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ const Text('字母导航:', style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(width: 10), Expanded( child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: _letters.map((letter) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: ChoiceChip( label: Text(letter), selected: _currentStickyLetter == letter, onSelected: (selected) => _jumpToSection(letter), ), ); }).toList(), ), ), ), ], ), ), ), ), // 内容区域:字母分组列表 SliverList( delegate: SliverChildBuilderDelegate( (context, index) { final letter = _letters[index]; if (!_data.containsKey(letter)) return const SizedBox(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 字母标题(非吸顶版本) Container( key: Key('header_$letter'), height: _headerHeight, color: Colors.blue[100], padding: const EdgeInsets.symmetric(horizontal: 16), alignment: Alignment.centerLeft, child: Text( letter, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ), // 列表项 ..._data[letter]!.map((item) => SizedBox( height: _itemHeight, child: ListTile( title: Text(item), leading: CircleAvatar(child: Text(letter)), ), )).toList(), // 字母组之间的分隔线 if (index < _letters.length - 1) const Divider(height: 20, thickness: 1), ], ); }, childCount: _letters.length, ), ), ], ), // 右侧字母导航栏 floatingActionButton: Container( width: 40, height: MediaQuery.of(context).size.height * 0.7, margin: const EdgeInsets.only(top: 80, bottom: 20), child: ListView.builder( physics: const NeverScrollableScrollPhysics(), itemCount: _letters.length, itemBuilder: (context, index) { final letter = _letters[index]; return GestureDetector( onTap: () => _jumpToSection(letter), child: Container( padding: const EdgeInsets.symmetric(vertical: 2), alignment: Alignment.center, child: Text( letter, style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: _data.containsKey(letter) ? (_currentStickyLetter == letter ? Colors.blue : Colors.black54) : Colors.grey, ), ), ), ); }, ), ), floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, // 三级吸顶:当前字母提示 [^1] persistentFooterButtons: _currentStickyLetter.isNotEmpty ? [ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.blue[100], borderRadius: BorderRadius.circular(20), ), child: Text( '当前: $_currentStickyLetter', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ) ] : null, ); } } // 自定义吸顶头部代理 [^1] class _StickyTabBarDelegate extends SliverPersistentHeaderDelegate { final Widget child; _StickyTabBarDelegate({required this.child}); @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { return child; } @override double get maxExtent => 50; @override double get minExtent => 50; @override bool shouldRebuild(covariant _StickyTabBarDelegate oldDelegate) { return child != oldDelegate.child; } } ``` ## 关键实现说明 ### 1. 三级吸顶结构设计 [^1] ```dart slivers: [ // 一级吸顶:应用标题栏 const SliverAppBar(pinned: true, ...), // 二级吸顶:字母导航标签栏 SliverPersistentHeader(pinned: true, ...), // 内容区域:字母分组列表 SliverList(...), ], ``` - **一级吸顶**:使用`SliverAppBar`实现可折叠的应用标题栏 - **二级吸顶**:使用`SliverPersistentHeader`实现字母导航标签栏 - **三级吸顶**:使用`persistentFooterButtons`实现当前字母提示 ### 2. 字母位置预计算与跳转 ```dart void _precalculatePositions() { double position = 0; for (final letter in _letters) { if (_data.containsKey(letter)) { _sectionPositions[letter] = position; position += _headerHeight + (_data[letter]!.length * _itemHeight); } } } void _jumpToSection(String letter) { final position = _sectionPositions[letter]; if (position != null) { _scrollController.jumpTo(position); } } ``` - 在布局完成后预计算所有字母组位置 - 使用`jumpTo`实现高效跳转 ### 3. 滚动监听与吸顶状态更新 ```dart void _handleScroll() { final double scrollPosition = _scrollController.position.pixels; String? newStickyLetter; // 查找当前应该吸顶的字母 for (final letter in _letters.reversed) { final position = _sectionPositions[letter]; if (position != null && scrollPosition >= position - _headerHeight) { newStickyLetter = letter; break; } } if (newStickyLetter != _currentStickyLetter) { setState(() { _currentStickyLetter = newStickyLetter ?? ''; }); } } ``` - 实时监听滚动位置 - 根据滚动位置计算当前应该吸顶的字母 - 动态更新吸顶状态 ### 4. 自定义吸顶头部代理 [^1] ```dart class _StickyTabBarDelegate extends SliverPersistentHeaderDelegate { @override Widget build(context, shrinkOffset, overlapsContent) => child; @override double get maxExtent => 50; @override double get minExtent => 50; @override bool shouldRebuild(covariant oldDelegate) => child != oldDelegate.child; } ``` - 自定义代理实现任意组件的吸顶功能 - 固定高度简化实现 - 支持灵活的内容更新 ## 吸顶效果展示 | 吸顶级别 | 组件 | 功能 | 实现方式 | |---------|------|------|---------| | 一级吸顶 | 应用标题栏 | 主应用标题,可折叠 | `SliverAppBar` | | 二级吸顶 | 字母导航标签栏 | 字母导航与当前字母提示 | `SliverPersistentHeader` | | 三级吸顶 | 当前字母提示 | 显示当前滚动到的字母 | `persistentFooterButtons` | ## 性能优化点 1. **位置预计算**:避免滚动时的实时计算 2. **固定高度**:所有列表项标题使用固定高度 3. **高效跳转**:使用`jumpTo`代替`animateTo` 4. **缓存扩展**:`cacheExtent: 1000`预加载屏幕外内容 5. **局部刷新**:仅更新吸顶状态相关的UI部分 ## 相关问题 1. 如何实现多级吸顶效果之间的联动动画? 2. Flutter中如何优化超长列表的滚动性能? 3. 吸顶效果在嵌套滚动视图中的实现方式有哪些? 4. 如何为吸顶组件添加渐变动画效果? 5. 在Flutter中如何实现类似通讯录的字母导航吸顶功能
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

talk_8

真诚赞赏,手有余香

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值