如何使用Flutter的NestedScrollView嵌套滚动视图

导读:在 Flutter 中,当页面需要同时包含“可折叠的头部”(如 SliverAppBar)与内部可滚动列表(如 TabBarView 列表)时,NestedScrollView 就成了首选。它可以将外部滚动与内部滚动无缝衔接,实现“头部先折叠,再滚动子列表”的效果。本文将从原理、代码示例、ASCII 图解等多个维度,带你一步步掌握 NestedScrollView 的使用方法与注意事项。

目录

  1. 为什么需要 NestedScrollView?问题场景与挑战
  2. NestedScrollView 的原理与结构

    • 2.1 Sliver 协作与滚动坐标系
    • 2.2 Outer Scroll 与 Inner Scroll 的协同机制
    • 2.3 ASCII 图解:NestedScrollView 滚动流程
  3. 基本用法:最简示例

    • 3.1 文件依赖与导入
    • 3.2 代码示例:SliverAppBar + TabBar + 列表
    • 3.3 关键代码解析
  4. 常见场景:带 TabBar 的可折叠头部

    • 4.1 SliverAppBar 属性详解(pinned、floating、snap)
    • 4.2 TabBarView 内部保持滚动位置独立
    • 4.3 代码示例与说明
    • 4.4 ASCII 图解:折叠头部与子列表滚动示意
  5. 高级技巧与注意事项

    • 5.1 避免滚动冲突与 physics 配置
    • 5.2 动态更新 Sliver 高度与 SliverOverlapAbsorber
    • 5.3 解决状态丢失:PageStorageKey
    • 5.4 手动控制滚动:ScrollController
  6. 示例项目:完整的新闻列表页面

    • 6.1 页面结构概览
    • 6.2 代码实现与注释
    • 6.3 效果截图与 ASCII 流程图
  7. 总结与最佳实践

一、为什么需要 NestedScrollView?问题场景与挑战

在移动端应用中,常见需求是在页面顶部有一个可滚动折叠的头部区域(例如头部横幅、轮播图、TabBar),而下方则是一个或多个可滚动的列表或内容区。我们希望满足以下交互效果:

  • 当用户在顶部区域向上滑动时,先将头部区域折叠缩小(或彻底隐藏),再滚动下方列表。
  • 当列表滚动到顶部时,继续向下拉动可以触发头部区域的展开。
  • 如果下方列表切换 Tab,需要保持每个 Tab 内列表的独立滚动状态。

如果直接将 SliverAppBar 与多个 ListView 嵌套,往往会出现滚动冲突、坐标错乱、滚动链断裂等问题。此时,Flutter 官方推荐使用 NestedScrollView 来自动协调“外部可滚动(Sliver 部分)”与“内部可滚动(Tab 内列表)”的滚动逻辑。

挑战点

  1. 滚动事件需要分发:当头部尚未完全折叠时,首先让外部滚动,否则才让内部列表滚动。
  2. 保持各 Tab 内部列表的滚动位置:切换 Tab 时,每个列表的滚动偏移要独立保存,回到上次位置。
  3. 与 Slivers 深度耦合:SliverAppBar、SliverPersistentHeader 等需要正确配置,才能在 NestedScrollView 中正常工作。

NestedScrollView 通过“协调两个 ScrollView 的滚动坐标系”来解决上述问题。下面深入了解其原理。


二、NestedScrollView 的原理与结构

2.1 Sliver 协作与滚动坐标系

在 Flutter 中,所有可滚动组件(ListViewCustomScrollViewGridView……)底层都是通过 Sliver(可滚动子部件)来实现。NestedScrollView 也基于 Sliver 构建:

  • NestedScrollView.headerSliverBuilder:返回一个 List<Widget>,其中的 Widget 必须是 Sliver(如 SliverAppBarSliverToBoxAdapterSliverPersistentHeader 等)。这部分被称为 outer slivers,即“外部可滚动区域”。
  • NestedScrollView.body:需要传入一个可滚动组件(常用 TabBarView + ListView 组合),即“内部可滚动区域”。

NestedScrollView 内部维护两个 ScrollController:(可通过属性覆盖)

  1. outerController:管理 headerSliverBuilder 构建的 Sliver 部分的滚动。
  2. innerControllers:管理 body 中每个可滚动子列表(如 ListViewGridView)的滚动。

它将这两个滚动坐标系通过一个“滚动通知协调机制”串联起来,确保:

  • 当 header 部分没有完全折叠时,所有滚动操作都由 outerController 消费;
  • 当 header 折叠到最小(通常是 TabBar 粘性在顶部的位置),后续滚动由当前 Tab 的 innerController 消费;
  • 当向下滚动并且 child 已经滚动到顶部(偏移为 0),再次向下滚动会触发 header 区域的展开(outerController 处理)。

2.2 Outer Scroll 与 Inner Scroll 的协同机制

大致流程如下:

  1. 初始状态outerController.offset = 0(header 展开),innerController[offset] = 0(列表顶部)。
  2. 向上滑动

    • 如果 outerController.offset < maxHeaderExtent,则滚动先由 outerController 消费,header 部分逐步折叠。
    • outerController.offset 达到 maxHeaderExtent 时,header 完全折叠,此时新的滚动事件传递给当前 innerController,使列表开始滚动。
  3. 向下滑动

    • 如果当前 innerController.offset > 0,则先让 innerController 向下滚动,直到 offset=0(列表回到顶部)。
    • 当 innerController.offset = 0 时,再次向下滚动事件才会传回给 outerController,让 header 区域展开。

这样就实现了“先折叠头部,再滚动子列表;先滚动子列表,再展开头部”的闭环。

2.3 ASCII 图解:NestedScrollView 滚动流程

|=============================|
|        Header Region        |  ← 外部 Sliver 部分(SliverAppBar、SliverPersistentHeader)
|   ┌───────────────────────┐ |  
|   │                       │ |
|   │    滚动前完整展开     │ |
|   │                       │ |
|   └───────────────────────┘ |
|=============================|
|       TabBar (粘性)         |  ← SliverPersistentHeader(pinned)
|=============================|
|      Body(当前 Tab 列表)   |  ← 内部可滚动 ListView
|  ┌───────────────────────┐  |
|  │    列表项 1           │  |
|  │    列表项 2           │  |
|  │    ...                │  |
|  └───────────────────────┘  |
|=============================|
  1. 向上滑动阶段 1

    • header 区域从完整展开逐步“折叠”,直到 TabBar 粘性顶端。
    • 坐标:outerOffset 从 0 → maxHeaderExtent
  2. 向上滑动阶段 2

    • header 已折叠,内部列表开始滚动,innerOffset 从 0 → …
  3. 向下滑动阶段 1

    • 如果内部列表不在顶部(innerOffset > 0),先让列表向下滚动到顶部(innerOffset → 0)。
  4. 向下滑动阶段 2

    • 内部列表已到顶部,新的向下滚动传递给 outer,使 header 展开(outerOffsetmaxHeaderExtent → 0)。

三、基本用法:最简示例

下面给出一个最基础的示例,演示如何使用 NestedScrollView 实现“可折叠头部 + TabBar + 列表”的结构。

3.1 文件依赖与导入

确保在 pubspec.yaml 中已引入 flutter/material.dart 即可,无需额外依赖。示例文件:pages/nested_scroll_example.dart

import 'package:flutter/material.dart';

3.2 代码示例:SliverAppBar + TabBar + 列表

// pages/nested_scroll_example.dart
import 'package:flutter/material.dart';

class NestedScrollExample extends StatefulWidget {
  const NestedScrollExample({Key? key}) : super(key: key);

  @override
  State<NestedScrollExample> createState() => _NestedScrollExampleState();
}

class _NestedScrollExampleState extends State<NestedScrollExample> with SingleTickerProviderStateMixin {
  late TabController _tabController;

  final List<String> _tabs = ['Tab1', 'Tab2', 'Tab3'];

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: _tabs.length, vsync: this);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        // 1. 外部 Sliver 区域
        headerSliverBuilder: (context, innerBoxIsScrolled) {
          return [
            SliverAppBar(
              title: const Text('NestedScrollView 示例'),
              expandedHeight: 200, // 展开高度
              pinned: true,        // 折叠后保留 AppBar
              flexibleSpace: FlexibleSpaceBar(
                background: Image.network(
                  'https://picsum.photos/800/400',
                  fit: BoxFit.cover,
                ),
              ),
              bottom: TabBar(
                controller: _tabController,
                tabs: _tabs.map((t) => Tab(text: t)).toList(),
              ),
            ),
          ];
        },
        // 2. 内部 Tab 页面
        body: TabBarView(
          controller: _tabController,
          children: _tabs.map((tab) {
            // 每个 tab 对应一个 ListView
            return ListView.builder(
              itemCount: 30,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text('$tab - 列表项 $index'),
                );
              },
            );
          }).toList(),
        ),
      ),
    );
  }
}

3.3 关键代码解析

  1. NestedScrollView

    • headerSliverBuilder:返回一个 Sliver 列表,此处只使用一个 SliverAppBar
    • body:必须是一个可滚动 Widget,此处我们用 TabBarView 包裹多个 ListView
  2. SliverAppBar

    • expandedHeight: 200:定义展开时的高度;
    • pinned: true:折叠后保留小尺寸 AppBar(及底部 TabBar);
    • flexibleSpace:可配置一个 FlexibleSpaceBar 作为伸缩背景;
    • bottom:在 AppBar 底部放置 TabBar,此时 TabBar 会在折叠后保持粘性。
  3. TabBarView + ListView

    • 每个 Tab 对应一个单独的 ListView.builder
    • NestedScrollView 会自动为每个 ListView 创建对应的 ScrollController,负责内部滚动;
  4. 滚动顺序

    • 当页面首次加载时,header 完全展开;用户往下拉时,列表滚到顶部才会触发表头下拉;
    • 当列表向上滚时,先折叠头部(outer),再滚动列表(inner)。

四、常见场景:带 TabBar 的可折叠头部

在实际开发中,最常见的 NestedScrollView 场景便是“可折叠头部(含轮播、Banner 或用户信息区)+ 粘性 TabBar + 各 Tab 列表”。下面进一步拆解与优化这一场景。

4.1 SliverAppBar 属性详解(pinned、floating、snap)

  • pinned: true

    • 意味着头部折叠后,AppBar 会一直粘在顶部显示,包括其 bottom(如 TabBar);
    • 如果想让 TabBar 保持可见,必须将 pinned 设为 true
  • floating: true

    • 使 AppBar 支持“下拉时立即出现”而非等到滚动到顶部才出现;
    • 如果同时设置 floating: truesnap: true,则向下拖动时,AppBar 会自动“弹跳”到展开状态;
  • snap: true

    • 只有在 floating: true 时才生效;
    • 当用户向下拽一小段距离后,AppBar 会直接“弹出”完整展开;

常见组合示例:

SliverAppBar(
  expandedHeight: 250,
  pinned: true,
  floating: true,
  snap: true,
  flexibleSpace: FlexibleSpaceBar(
    title: const Text('Profile'),
    background: Image.network('https://picsum.photos/800/400', fit: BoxFit.cover),
  ),
  bottom: TabBar(...),
)
  • 效果对比

    1. pinned: true → 折叠后 TabBar 固定在顶部;
    2. floating: true + snap: true → 当用户向下滑动时,AppBar 会“跟随”手势快速显示,并在松手时弹出整个头部。

4.2 TabBarView 内部保持滚动位置独立

通过 NestedScrollView,每个 Tab 内部的 ListView 会自动拥有各自的 ScrollController,因此切换 Tab 后:

  • 上次滚动到哪里,下次切回仍停留在同一位置;
  • 如果想在切换 Tab 时将上一个 Tab 滚动位置重置,可手动操作对应的 ScrollController

如果你需要自定义 Controller,可为 NestedScrollView 提供 controller: ScrollController(),但通常不必手动控制外部滚动。内部列表若需指定固定 PageStorageKey,可在 ListView.builder 中赋予相同的 key,以保证切换时滚动状态不丢失:

ListView.builder(
  key: PageStorageKey('TabListView_$tabIndex'),
  itemCount: 50,
  itemBuilder: (context, idx) => ListTile(...),
)

4.3 代码示例与说明

以下示例在前面基础上,加入 floatingsnap,并为每个列表加上 PageStorageKey

// pages/nested_scroll_tabbar_demo.dart
import 'package:flutter/material.dart';

class NestedScrollTabDemo extends StatefulWidget {
  const NestedScrollTabDemo({Key? key}) : super(key: key);

  @override
  State<NestedScrollTabDemo> createState() => _NestedScrollTabDemoState();
}

class _NestedScrollTabDemoState extends State<NestedScrollTabDemo> with SingleTickerProviderStateMixin {
  late TabController _tabController;
  final List<String> _tabs = ['推荐', '热门', '最新'];

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: _tabs.length, vsync: this);
  }

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

  Widget _buildList(String tag) {
    return Builder(
      builder: (context) {
        return ListView.builder(
          key: PageStorageKey('list_$tag'),
          itemCount: 20,
          itemBuilder: (context, index) {
            return ListTile(
              title: Text('$tag 文章 $index'),
              leading: CircleAvatar(child: Text('$index')),
            );
          },
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        // 保持 Tab 内容与外部头部协同滚动
        headerSliverBuilder: (context, innerBoxIsScrolled) {
          return [
            SliverAppBar(
              title: const Text('新闻动态'),
              expandedHeight: 180,
              pinned: true,
              floating: true,
              snap: true,
              flexibleSpace: FlexibleSpaceBar(
                background: Image.asset('assets/banner.jpg', fit: BoxFit.cover),
              ),
              bottom: TabBar(
                controller: _tabController,
                tabs: _tabs.map((t) => Tab(text: t)).toList(),
              ),
            ),
          ];
        },
        body: TabBarView(
          controller: _tabController,
          children: _tabs.map((t) => _buildList(t)).toList(),
        ),
      ),
    );
  }
}
  • 要点总结

    1. floating: true + snap: true → 提示“头部跟随下拉手势并弹出”;
    2. ListView 中设置 PageStorageKey 能确保每个 Tab 的滚动位置独立且不会丢失;
    3. NestedScrollView 会根据内部 ListView 的滚动状态自动决定是否折叠/展开头部。

4.4 ASCII 图解:折叠头部与子列表滚动示意

┌────────────────────────────────────────┐
│         [ SliverAppBar - Banner ]      │   ← 初始展开状态 (高度=180)
│        ┌───────────────────────┐       │
│        │   Banner 图片         │       │
│        └───────────────────────┘       │
│                                        │
│   [ TabBar: 推荐 | 热门 | 最新 ]         │   ← 始终粘性在顶部
├────────────────────────────────────────┤
│  Tab1 列表:                            │   ← 列表区域 (ListView 依次排列)
│  ┌───────────────────────────────────┐ │
│  │ 文章 0                            │ │
│  │ 文章 1                            │ │
│  │ ...                               │ │
│  └───────────────────────────────────┘ │
│                                        │
└────────────────────────────────────────┘

用户向上滑:
1. Banner 区域先折叠 (SliverAppBar offset 0→180)
2. Banner 折叠完成后,列表开始滚动 (ListView offset 0→…)
3. TabBar 粘性贴住顶部,可随 Tab 切换

五、高级技巧与注意事项

5.1 避免滚动冲突与 physics 配置

有时业务需求需要在内部嵌套更多滚动组件(如 GridViewCustomScrollView 等),若不正确配置 physics,可能会导致滚动冲突或滑动卡顿。

  • 禁用内部滚动弹性(特别在 iOS 上):

    ListView.builder(
      physics: const ClampingScrollPhysics(), // 或 NeverScrollableScrollPhysics()
      itemCount: …,
      itemBuilder: …,
    );
  • 让 NestedScrollView 自己处理滚动:在内部列表使用 AlwaysScrollableScrollPhysics(),确保即使列表很短也能向下拉触发头部展开。

5.2 动态更新 Sliver 高度与 SliverOverlapAbsorber

当头部高度需要根据业务逻辑动态变化(如网络请求得到用户头像高度后,再决定 SliverAppBar 展开高度),可通过 SliverOverlapAbsorberSliverOverlapInjector 来正确处理重叠距离,避免列表内容被头部遮挡。

示例简要:

NestedScrollView(
  headerSliverBuilder: (context, innerBoxIsScrolled) {
    return [
      SliverOverlapAbsorber(
        handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
        sliver: SliverAppBar(
          expandedHeight: dynamicHeight,
          // ...
        ),
      ),
    ];
  },
  body: Builder(
    builder: (context) {
      return CustomScrollView(
        slivers: [
          SliverOverlapInjector(
            handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (context, index) => ListTile(title: Text('Item $index')),
              childCount: 30,
            ),
          ),
        ],
      );
    },
  ),
)
  • 原理SliverOverlapAbsorber 会吸收头部实际占据的像素高度,SliverOverlapInjector 再将这部分重叠高度注入给内部 Sliver,以防止内容被遮挡。

5.3 解决状态丢失:PageStorageKey

NestedScrollView + TabBarView + ListView 场景下,如果不使用 key,切换 Tab 后可能会重新构建列表,导致滚动位置丢失。为避免该问题,给每个内部列表设置唯一的 PageStorageKey,Flutter 会自动保存并恢复其滚动偏移。

ListView.builder(
  key: PageStorageKey('tab_${tabIndex}_list'),
  itemCount: 50,
  itemBuilder: …,
)

5.4 手动控制滚动:ScrollController

在某些场景,需要程序主动滚动到某个位置,比如点击一个按钮后将列表滚到顶部,同时也收起头部,这时可以通过 ScrollController 联动外部与内部滚动。

// 定义两个控制器
final _outerController = ScrollController();
final _innerController = ScrollController();

// 在 NestedScrollView 中指定
NestedScrollView(
  controller: _outerController,
  // ...
  body: TabBarView(
    children: [
      ListView(controller: _innerController, …),
      // ...
    ],
  ),
);

// 在某处调用:
void scrollToTop() {
  // 先让内部列表滚到顶部
  _innerController.animateTo(0, duration: Duration(milliseconds: 300), curve: Curves.ease);
  // 再让外部头部展开
  _outerController.animateTo(0, duration: Duration(milliseconds: 300), curve: Curves.ease);
}

六、示例项目:完整的新闻列表页面

6.1 页面结构概览

新闻列表页面
├─ SliverAppBar (带Banner+TabBar)
│   ├─ FlexibleSpaceBar (Banner 图片)
│   └─ TabBar (推荐|热门|最新)
└─ TabBarView
    ├─ Tab1:ListView (文章列表)
    ├─ Tab2:ListView (文章列表)
    └─ Tab3:ListView (文章列表)

6.2 代码实现与注释

// lib/pages/news_page.dart
import 'package:flutter/material.dart';

class NewsPage extends StatefulWidget {
  const NewsPage({Key? key}) : super(key: key);

  @override
  State<NewsPage> createState() => _NewsPageState();
}

class _NewsPageState extends State<NewsPage> with SingleTickerProviderStateMixin {
  late TabController _tabController;
  final List<String> _tabs = ['推荐', '热门', '最新'];
  final ScrollController _outerCtrl = ScrollController();

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: _tabs.length, vsync: this);
  }

  @override
  void dispose() {
    _tabController.dispose();
    _outerCtrl.dispose();
    super.dispose();
  }

  // 构建每个 Tab 内的文章列表
  Widget _buildArticleList(String category) {
    return ListView.builder(
      key: PageStorageKey('articleList_$category'),
      itemCount: 25,
      itemBuilder: (context, index) {
        return Card(
          margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
          child: ListTile(
            title: Text('$category 文章标题 $index'),
            subtitle: const Text('这是一段摘要,展示文章简介。'),
          ),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        controller: _outerCtrl,
        headerSliverBuilder: (context, innerScrolled) {
          return [
            // 头部 Banner + TabBar
            SliverAppBar(
              expandedHeight: 200,
              pinned: true,
              floating: true,
              snap: true,
              backgroundColor: Colors.blueAccent,
              flexibleSpace: FlexibleSpaceBar(
                title: const Text('新闻动态'),
                background: Image.network('https://picsum.photos/800/400', fit: BoxFit.cover),
              ),
              bottom: TabBar(
                controller: _tabController,
                indicatorColor: Colors.white,
                tabs: _tabs.map((t) => Tab(text: t)).toList(),
              ),
            ),
          ];
        },
        body: TabBarView(
          controller: _tabController,
          children: _tabs.map((t) => _buildArticleList(t)).toList(),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // 点击按钮时,内外滚动都重置到顶部
          _outerCtrl.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.ease);
        },
        child: const Icon(Icons.arrow_upward),
      ),
    );
  }
}
  • 解释

    1. SliverAppBar(expandedHeight: 200, pinned: true, floating: true, snap: true):实现可折叠 Banner,并在下拉时快速弹出;
    2. TabBar 放在 bottom 部分,折叠后仍保持可见;
    3. TabBarView 内部每个 ListView 通过 PageStorageKey 保持滚动位置;
    4. FloatingActionButton 可快速一键回到顶部(「收起头部」+「滚回列表顶部」);

6.3 效果截图与 ASCII 流程图

┌──────────────────────────────────────────────┐
│             [ Banner 图片 展示 ]             │  ← SliverAppBar (高度 200)
│                                              │
│     当向上滑动时,会先逐步折叠 Banner 区域      │
│                                              │
│──────────────────────────────────────────────│
│ [TabBar: 推荐 | 热门 | 最新 ]  ← 折叠后保留    │
│──────────────────────────────────────────────│
│                                              │
│ 推荐 文章列表 ...                             │
│  文章卡片 0                                   │
│  文章卡片 1                                   │
│  ...                                          │
│──────────────────────────────────────────────│
│                                              │
└──────────────────────────────────────────────┘
  • 滚动示意

    1. 向上滚动:Banner 高度 200 → 0(完全折叠),此过程由 outerCtrl 消费;
    2. Banner 折叠完成后:TabBar 粘性置顶,此时 ListView 内部继续滚动,由对应的 ScrollController 消费;
    3. 向下滚动:先将 ListView 滚回到顶部(如果不在顶部),然后才触发 Banner 展开(outerCtrl 消费)。

七、总结与最佳实践

  1. 嵌套滚动场景首选 NestedScrollView

    • 当页面有“可折叠头部 + 子列表滚动”需求时,优先考虑 NestedScrollView,它能自动协调外部 Sliver 与内部列表的滚动事件,避免手动处理滚动冲突。
  2. 合理配置 SliverAppBar 属性

    • pinned: true → 折叠后保留 AppBar 与 TabBar;
    • floating: true + snap: true → 下拉时头部跟随并弹跳展开;
    • 根据体验需求做取舍。
  3. 保证内部列表滚动状态独立

    • ListView 等列表组件设置 PageStorageKey,避免切换 Tab 时滚动位置丢失。
  4. 注意滚动冲突与 physics 配置

    • 如果内部列表过短或需要一直撑开才能下拉展开头部,给 ListView 设置 physics: AlwaysScrollableScrollPhysics()
    • 在 iOS 平台,如果不希望出现弹性效果,可使用 ClampingScrollPhysics()
  5. 动态头部高度推荐使用 SliverOverlapAbsorber / Injector

    • 当头部高度需动态变化时,应配合 SliverOverlapAbsorberSliverOverlapInjector,以保证内部 Sliver 正确偏移,避免内容被遮挡。
  6. 可选手动联动 ScrollController

    • 在需要程序触发“回到顶部”或“展开头部”等业务场景,可通过外部与内部的 ScrollController 进行手动协同滚动。

通过本文的原理剖析代码示例ASCII 图解,你已经掌握了如何在 Flutter 中使用 NestedScrollView 实现嵌套滚动场景。只需将上述思路带到项目中,便能轻松完成“可折叠头部 + 粘性 TabBar + 多列表”的复杂布局。

最后修改于:2025年06月03日 15:02

评论已关闭

推荐阅读

DDPG 模型解析,附Pytorch完整代码
2024年11月24日
DQN 模型解析,附Pytorch完整代码
2024年11月24日
AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日