向下滚动时隐藏的 Flutter TabBar 和 SliverAppBar
Flutter TabBar and SliverAppBar that hides when you scroll down
我正在尝试创建一个带有顶部应用程序栏和下方标签栏的应用程序。当您向下滚动时,应用程序栏应该通过移出屏幕而隐藏(但标签应该保留),当您向上滚动时,应用程序栏应该再次显示。这种行为可以在 WhatsApp 中看到。请参阅 this video for a demonstration. (Taken from Material.io). This 是一个类似的行为,虽然应用栏和标签栏在滚动时隐藏,所以这不是我正在寻找的行为。
我已经实现了自动隐藏,但是有几个问题:
我必须将 SliverAppBar
的 snap
设置为 true
。没有这个,当我向上滚动时,应用程序栏将不会显示。
虽然这是有效的,但这不是我想要的行为。我希望应用程序栏能够流畅地显示(类似于 WhatsApp),而不是即使滚动很小也能看到。
澄清一下,当我一直向下滚动时,即使向上滚动很少,应用栏也应该会出现。我不想一直向上滚动才能看到应用栏。
当我向下滚动并切换标签时,有一小部分内容被剪切掉了。
下面是显示该行为的 GIF:
(看到我在listView(tab1)向下滚动时的部分,然后回到tab2)
这是 DefaultTabController
的代码:
DefaultTabController(
length: 2,
child: new Scaffold(
body: new NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
new SliverAppBar(
title: Text("Application"),
floating: true,
pinned: true,
snap: true, // <--- this is required if I want the application bar to show when I scroll up
bottom: new TabBar(
tabs: [ ... ], // <-- total of 2 tabs
),
),
];
},
body: new TabBarView(
children: [ ... ] // <--- the array item is a ListView
),
),
),
),
如果需要,完整代码在GitHub repository. main.dart
is here。
我还发现了这个相关问题:。但是,它没有提供解决方案。同样的问题仍然存在,向上滚动时,SliverAppBar
不会显示。 (所以 snap: true
是必需的)
我还在 Flutter 的 GitHub 上找到了 this issue。 (编辑:有人评论说他们正在等待Flutter团队解决这个问题。是否有可能没有解决方案?)
这是 flutter doctor -v
的输出:Pastebin。发现了某些问题,但据我了解,它们应该不会产生影响。
编辑:这有两个问题:
--- 编辑 1 --
好的,所以我快速为您准备了一些东西。我阅读了这篇文章(由 Flutter 的主要开发者之一 Emily Fortuna 撰写)以更好地理解 Slivers。
但后来发现这个 Youtube 视频基本上使用了你的代码,所以我选择了这个,而不是试图弄清楚关于 Slivers 的每一个小细节。
Youtube: Using Tab and Scroll Controllers and the NestedScrollView in Dart's Flutter Framework
事实证明,您的代码走在了正确的轨道上。您可以在 NestedScrollView
中使用 SliverAppBar
(我上次尝试时不是这种情况),但我做了一些更改。我将在我的代码之后解释:
import 'package:flutter/material.dart';
import 'dart:math';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin /*<-- This is for the controllers*/ {
TabController _tabController; // To control switching tabs
ScrollController _scrollViewController; // To control scrolling
List<String> items = [];
List<Color> colors = [Colors.red, Colors.green, Colors.yellow, Colors.purple, Colors.blue, Colors.amber, Colors.cyan, Colors.pink];
Random random = new Random();
Color getRandomColor() {
return colors.elementAt(random.nextInt(colors.length));
}
@override
void initState() {
super.initState();
_tabController =TabController(vsync: this, length: 2);
_scrollViewController =ScrollController();
}
@override
void dispose() {
super.dispose();
_tabController.dispose();
_scrollViewController.dispose();
}
@override
Widget build(BuildContext context) {
// Init the items
for (var i = 0; i < 100; i++) {
items.add('Item $i');
}
return SafeArea(
child: NestedScrollView(
controller: _scrollViewController,
headerSliverBuilder: (BuildContext context, bool boxIsScrolled) {
return <Widget>[
SliverAppBar(
title: Text("WhatsApp using Flutter"),
floating: true,
pinned: false,
snap: true,
bottom: TabBar(
tabs: <Widget>[
Tab(
child: Text("Colors"),
),
Tab(
child: Text("Chats"),
),
],
controller: _tabController,
),
),
];
},
body: TabBarView(
controller: _tabController,
children: <Widget>[
ListView.builder(
itemBuilder: (BuildContext context, int index) {
Color color = getRandomColor();
return Container(
height: 150.0,
color: color,
child: Text(
"Row $index",
style: TextStyle(
color: Colors.white,
),
),
);
},
//physics: NeverScrollableScrollPhysics(), //This may come in handy if you have issues with scrolling in the future
),
ListView.builder(
itemBuilder: (BuildContext context, int index) {
return Material(
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blueGrey,
),
title: Text(
items.elementAt(index)
),
),
);
},
//physics: NeverScrollableScrollPhysics(),
),
],
),
),
);
}
}
好了继续解释
使用一个StatefulWidget
Flutter 中的大多数小部件都是有状态的,但这取决于具体情况。我认为在这种情况下更好,因为您使用的是 ListView
,它可能会随着用户添加或删除 conversations/chats.
而改变
SafeArea
因为这个小部件很棒。
上阅读它
控制者
我认为这起初是个大问题,但也许是其他问题。但是如果你在 Flutter 中处理自定义行为,你通常应该制作自己的控制器。所以我制作了 _tabController
和 _scrollViewController
(我不认为我从它们中获得了所有功能,即跟踪选项卡之间的滚动位置,但它们适用于基础知识)。您用于 TabBar
和 TabView
的选项卡控制器应该相同。
ListTile
之前的Material
小部件
您可能迟早会发现这一点,但是 ListTile
小部件是一个 Material 小部件,因此根据我在尝试渲染时得到的输出需要一个 "Material ancestor widget"起初。所以我用那个让你有点头疼。我认为这是因为我没有使用 Scaffold
。 (当您使用没有 Material 祖先小部件的 Material 小部件时,请记住这一点)
希望这可以帮助您入门,如果您需要任何帮助,请给我发消息或将我添加到您的 Github 存储库中,我会看看我能做些什么。
--- 原创 ---
我也在 Reddit 上回答了你,希望你能尽快看到这两个中的一个。
SliverAppBar 信息
您希望 SliverAppBar 具有的关键属性是:
floating: Whether the app bar should become visible as soon as the user scrolls towards the app bar.
pinned: Whether the app bar should remain visible at the start of the scroll view. (This is the one you are asking about)
snap: If snap and floating are true then the floating app bar will "snap" into view.
这一切都来自Flutter SliverAppBar Docs。他们有很多动画示例,其中包含浮动、固定和捕捉的不同组合。
因此对于您来说,以下应该有效:
SliverAppBar(
title: Text("Application"),
floating: true, // <--- this is required if you want the appbar to come back into view when you scroll up
pinned: false, // <--- this will make the appbar disappear on scrolling down
snap: true, // <--- this is required if you want the application bar to 'snap' when you scroll up (floating MUST be true as well)
bottom: new TabBar(
tabs: [ ... ], // <-- total of 2 tabs
),
),
带有 SliverAppBar 的滚动视图
回答NestedScrollView
的基本问题。根据文档(同上),SliverAppBar
是:
A material design app bar that integrates with a CustomScrollView
.
因此您不能使用 NestedScrollView
您需要使用 CustomScrollView
。 这是 Sliver
[=120] 的预期用途=] 但它们可以用在 NestedScrollView
查看 docs.
您需要使用SliverOverlapAbsorber/SliverOverlapInjector, the following code works for me (Full Code):
@override
Widget build(BuildContext context) {
return Material(
child: Scaffold(
body: DefaultTabController(
length: _tabs.length, // This is the number of tabs.
child: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
// These are the slivers that show up in the "outer" scroll view.
return <Widget>[
SliverOverlapAbsorber(
// This widget takes the overlapping behavior of the SliverAppBar,
// and redirects it to the SliverOverlapInjector below. If it is
// missing, then it is possible for the nested "inner" scroll view
// below to end up under the SliverAppBar even when the inner
// scroll view thinks it has not been scrolled.
// This is not necessary if the "headerSliverBuilder" only builds
// widgets that do not overlap the next sliver.
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverSafeArea(
top: false,
sliver: SliverAppBar(
title: const Text('Books'),
floating: true,
pinned: true,
snap: false,
primary: true,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
// These are the widgets to put in each tab in the tab bar.
tabs: _tabs.map((String name) => Tab(text: name)).toList(),
),
),
),
),
];
},
body: TabBarView(
// These are the contents of the tab views, below the tabs.
children: _tabs.map((String name) {
return SafeArea(
top: false,
bottom: false,
child: Builder(
// This Builder is needed to provide a BuildContext that is "inside"
// the NestedScrollView, so that sliverOverlapAbsorberHandleFor() can
// find the NestedScrollView.
builder: (BuildContext context) {
return CustomScrollView(
// The "controller" and "primary" members should be left
// unset, so that the NestedScrollView can control this
// inner scroll view.
// If the "controller" property is set, then this scroll
// view will not be associated with the NestedScrollView.
// The PageStorageKey should be unique to this ScrollView;
// it allows the list to remember its scroll position when
// the tab view is not on the screen.
key: PageStorageKey<String>(name),
slivers: <Widget>[
SliverOverlapInjector(
// This is the flip side of the SliverOverlapAbsorber above.
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(
context),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
// In this example, the inner scroll view has
// fixed-height list items, hence the use of
// SliverFixedExtentList. However, one could use any
// sliver widget here, e.g. SliverList or SliverGrid.
sliver: SliverFixedExtentList(
// The items in this example are fixed to 48 pixels
// high. This matches the Material Design spec for
// ListTile widgets.
itemExtent: 60.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
// This builder is called for each child.
// In this example, we just number each list item.
return Container(
color: Color((math.Random().nextDouble() *
0xFFFFFF)
.toInt() <<
0)
.withOpacity(1.0));
},
// The childCount of the SliverChildBuilderDelegate
// specifies how many children this inner list
// has. In this example, each tab has a list of
// exactly 30 items, but this is arbitrary.
childCount: 30,
),
),
),
],
);
},
),
);
}).toList(),
),
),
),
),
);
}
通过将 SliverAppbar 与 NestedScrollView 结合使用,我能够使带有 Tabbar 的浮动 Appbar 类似于 WhatsApp。
在 NestedScrollView 和
中添加 floatHeaderSlivers: true
固定:true,浮动:true,在 SliverAppBar
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: CustomSliverAppbar(),
);
}
}
class CustomSliverAppbar extends StatefulWidget {
@override
_CustomSliverAppbarState createState() => _CustomSliverAppbarState();
}
class _CustomSliverAppbarState extends State<CustomSliverAppbar>
with SingleTickerProviderStateMixin {
TabController _tabController;
@override
void initState() {
_tabController = TabController(
initialIndex: 0,
length: 2,
vsync: this,
);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
title: Text(
"WhatsApp type sliver appbar",
),
centerTitle: true,
pinned: true,
floating: true,
bottom: TabBar(
indicatorColor: Colors.black,
labelPadding: const EdgeInsets.only(
bottom: 16,
),
controller: _tabController,
tabs: [
Text("TAB A"),
Text("TAB B"),
]),
),
];
},
body: TabBarView(
controller: _tabController,
children: [
TabA(),
const Center(
child: Text('Display Tab 2',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
],
),
),
);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
}
class TabA extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scrollbar(
child: ListView.separated(
separatorBuilder: (context, child) => Divider(
height: 1,
),
padding: EdgeInsets.all(0.0),
itemCount: 30,
itemBuilder: (context, i) {
return Container(
height: 100,
width: double.infinity,
color: Colors.primaries[Random().nextInt(Colors.primaries.length)],
);
},
),
);
}
}
更新 - Sliver 应用栏扩展
如果你想看到 Sliver App Bar 在有人向上滚动时展开,即不是一直滚动到顶部而是一点点,那么只需将代码中的 snap: false
更改为 snap: true
: )
解决方案[修复所有点]
冲浪 google、Whosebug、github 问题、reddit 几个小时。我终于可以想出一个解决以下问题的解决方案:
Sliver 应用栏,标题被隐藏,向下滚动后只有标签栏可见。当你到达顶部时,你会再次看到标题。
主要:当您在选项卡 1 中滚动然后导航到选项卡 2 时,您不会看到任何重叠。 Tab 2的内容不会被Sliver App bar遮挡
List 中最顶部元素的 Sliver Padding 为 0。
在单个选项卡中保留滚动位置
下面是代码,我会尝试解释一下 (dartpad preview) :
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyStatelessWidget(),
);
}
}
class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final List<String> _tabs = <String>['Tab 1', 'Tab 2'];
return DefaultTabController(
length: _tabs.length,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: const Text('Books'),
floating: true,
pinned: true,
snap: false,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
tabs: _tabs.map((String name) => Tab(text: name)).toList(),
),
),
),
];
},
body: TabBarView(
children: _tabs.map((String name) {
return SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (BuildContext context) {
return CustomScrollView(
key: PageStorageKey<String>(name),
slivers: <Widget>[
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
);
},
childCount: 30,
),
),
),
],
);
},
),
);
}).toList(),
),
),
),
);
}
}
在 dartpad 中测试你想要的一切,一旦你没问题,那么让我们试着了解这里发生了什么。
大部分代码来自flutter documentation of NestedScrollView
他们在评论中提到得很好。我不是专家,所以我只强调我认为解决了大部分问题的方法。
我认为这里有两点很关键:
SliverOverlapAbsorber
& SliverOverlapInjector
- 使用
SliverList
代替 ListView
无论我们看到什么额外的 space 或 space 应用栏消耗和第一个列表项重叠主要通过使用以上两点解决。
为了记住选项卡的滚动位置,他们在 CustomScrollView
:
中添加了 PageStorageKey
key: PageStorageKey<String>(name),
name
只是一个字符串 -> 'Tab 1'
他们还在文档中提到我们可以使用 SliverFixedExtentList、SliverGrid,基本上是 Sliver
小部件。现在应该在需要时使用 Sliver 小部件。在一个 Flutter Youtube 视频(官方频道)中,他们提到 ListView、GridView 都是 Sliver 的高级实现。所以如果你想超级定制滚动或外观行为,Slivers 是低级的东西。
如果我遗漏了什么或说错了,请在评论中告诉我。
我正在尝试创建一个带有顶部应用程序栏和下方标签栏的应用程序。当您向下滚动时,应用程序栏应该通过移出屏幕而隐藏(但标签应该保留),当您向上滚动时,应用程序栏应该再次显示。这种行为可以在 WhatsApp 中看到。请参阅 this video for a demonstration. (Taken from Material.io). This 是一个类似的行为,虽然应用栏和标签栏在滚动时隐藏,所以这不是我正在寻找的行为。
我已经实现了自动隐藏,但是有几个问题:
我必须将
SliverAppBar
的snap
设置为true
。没有这个,当我向上滚动时,应用程序栏将不会显示。虽然这是有效的,但这不是我想要的行为。我希望应用程序栏能够流畅地显示(类似于 WhatsApp),而不是即使滚动很小也能看到。
澄清一下,当我一直向下滚动时,即使向上滚动很少,应用栏也应该会出现。我不想一直向上滚动才能看到应用栏。
当我向下滚动并切换标签时,有一小部分内容被剪切掉了。
下面是显示该行为的 GIF:
(看到我在listView(tab1)向下滚动时的部分,然后回到tab2)
这是 DefaultTabController
的代码:
DefaultTabController(
length: 2,
child: new Scaffold(
body: new NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
new SliverAppBar(
title: Text("Application"),
floating: true,
pinned: true,
snap: true, // <--- this is required if I want the application bar to show when I scroll up
bottom: new TabBar(
tabs: [ ... ], // <-- total of 2 tabs
),
),
];
},
body: new TabBarView(
children: [ ... ] // <--- the array item is a ListView
),
),
),
),
如果需要,完整代码在GitHub repository. main.dart
is here。
我还发现了这个相关问题:SliverAppBar
不会显示。 (所以 snap: true
是必需的)
我还在 Flutter 的 GitHub 上找到了 this issue。 (编辑:有人评论说他们正在等待Flutter团队解决这个问题。是否有可能没有解决方案?)
这是 flutter doctor -v
的输出:Pastebin。发现了某些问题,但据我了解,它们应该不会产生影响。
编辑:这有两个问题:
--- 编辑 1 --
好的,所以我快速为您准备了一些东西。我阅读了这篇文章(由 Flutter 的主要开发者之一 Emily Fortuna 撰写)以更好地理解 Slivers。
但后来发现这个 Youtube 视频基本上使用了你的代码,所以我选择了这个,而不是试图弄清楚关于 Slivers 的每一个小细节。
Youtube: Using Tab and Scroll Controllers and the NestedScrollView in Dart's Flutter Framework
事实证明,您的代码走在了正确的轨道上。您可以在 NestedScrollView
中使用 SliverAppBar
(我上次尝试时不是这种情况),但我做了一些更改。我将在我的代码之后解释:
import 'package:flutter/material.dart';
import 'dart:math';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin /*<-- This is for the controllers*/ {
TabController _tabController; // To control switching tabs
ScrollController _scrollViewController; // To control scrolling
List<String> items = [];
List<Color> colors = [Colors.red, Colors.green, Colors.yellow, Colors.purple, Colors.blue, Colors.amber, Colors.cyan, Colors.pink];
Random random = new Random();
Color getRandomColor() {
return colors.elementAt(random.nextInt(colors.length));
}
@override
void initState() {
super.initState();
_tabController =TabController(vsync: this, length: 2);
_scrollViewController =ScrollController();
}
@override
void dispose() {
super.dispose();
_tabController.dispose();
_scrollViewController.dispose();
}
@override
Widget build(BuildContext context) {
// Init the items
for (var i = 0; i < 100; i++) {
items.add('Item $i');
}
return SafeArea(
child: NestedScrollView(
controller: _scrollViewController,
headerSliverBuilder: (BuildContext context, bool boxIsScrolled) {
return <Widget>[
SliverAppBar(
title: Text("WhatsApp using Flutter"),
floating: true,
pinned: false,
snap: true,
bottom: TabBar(
tabs: <Widget>[
Tab(
child: Text("Colors"),
),
Tab(
child: Text("Chats"),
),
],
controller: _tabController,
),
),
];
},
body: TabBarView(
controller: _tabController,
children: <Widget>[
ListView.builder(
itemBuilder: (BuildContext context, int index) {
Color color = getRandomColor();
return Container(
height: 150.0,
color: color,
child: Text(
"Row $index",
style: TextStyle(
color: Colors.white,
),
),
);
},
//physics: NeverScrollableScrollPhysics(), //This may come in handy if you have issues with scrolling in the future
),
ListView.builder(
itemBuilder: (BuildContext context, int index) {
return Material(
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blueGrey,
),
title: Text(
items.elementAt(index)
),
),
);
},
//physics: NeverScrollableScrollPhysics(),
),
],
),
),
);
}
}
好了继续解释
使用一个
StatefulWidget
Flutter 中的大多数小部件都是有状态的,但这取决于具体情况。我认为在这种情况下更好,因为您使用的是
ListView
,它可能会随着用户添加或删除 conversations/chats. 而改变
SafeArea
因为这个小部件很棒。 上阅读它
控制者
我认为这起初是个大问题,但也许是其他问题。但是如果你在 Flutter 中处理自定义行为,你通常应该制作自己的控制器。所以我制作了
_tabController
和_scrollViewController
(我不认为我从它们中获得了所有功能,即跟踪选项卡之间的滚动位置,但它们适用于基础知识)。您用于TabBar
和TabView
的选项卡控制器应该相同。
之前的ListTile
Material
小部件您可能迟早会发现这一点,但是
ListTile
小部件是一个 Material 小部件,因此根据我在尝试渲染时得到的输出需要一个 "Material ancestor widget"起初。所以我用那个让你有点头疼。我认为这是因为我没有使用Scaffold
。 (当您使用没有 Material 祖先小部件的 Material 小部件时,请记住这一点)
希望这可以帮助您入门,如果您需要任何帮助,请给我发消息或将我添加到您的 Github 存储库中,我会看看我能做些什么。
--- 原创 ---
我也在 Reddit 上回答了你,希望你能尽快看到这两个中的一个。
SliverAppBar 信息
您希望 SliverAppBar 具有的关键属性是:
floating: Whether the app bar should become visible as soon as the user scrolls towards the app bar.
pinned: Whether the app bar should remain visible at the start of the scroll view. (This is the one you are asking about)
snap: If snap and floating are true then the floating app bar will "snap" into view.
这一切都来自Flutter SliverAppBar Docs。他们有很多动画示例,其中包含浮动、固定和捕捉的不同组合。
因此对于您来说,以下应该有效:
SliverAppBar(
title: Text("Application"),
floating: true, // <--- this is required if you want the appbar to come back into view when you scroll up
pinned: false, // <--- this will make the appbar disappear on scrolling down
snap: true, // <--- this is required if you want the application bar to 'snap' when you scroll up (floating MUST be true as well)
bottom: new TabBar(
tabs: [ ... ], // <-- total of 2 tabs
),
),
带有 SliverAppBar 的滚动视图
回答NestedScrollView
的基本问题。根据文档(同上),SliverAppBar
是:
A material design app bar that integrates with a
CustomScrollView
.
因此您不能使用 这是 NestedScrollView
您需要使用 CustomScrollView
。Sliver
[=120] 的预期用途=] 但它们可以用在 NestedScrollView
查看 docs.
您需要使用SliverOverlapAbsorber/SliverOverlapInjector, the following code works for me (Full Code):
@override
Widget build(BuildContext context) {
return Material(
child: Scaffold(
body: DefaultTabController(
length: _tabs.length, // This is the number of tabs.
child: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
// These are the slivers that show up in the "outer" scroll view.
return <Widget>[
SliverOverlapAbsorber(
// This widget takes the overlapping behavior of the SliverAppBar,
// and redirects it to the SliverOverlapInjector below. If it is
// missing, then it is possible for the nested "inner" scroll view
// below to end up under the SliverAppBar even when the inner
// scroll view thinks it has not been scrolled.
// This is not necessary if the "headerSliverBuilder" only builds
// widgets that do not overlap the next sliver.
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverSafeArea(
top: false,
sliver: SliverAppBar(
title: const Text('Books'),
floating: true,
pinned: true,
snap: false,
primary: true,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
// These are the widgets to put in each tab in the tab bar.
tabs: _tabs.map((String name) => Tab(text: name)).toList(),
),
),
),
),
];
},
body: TabBarView(
// These are the contents of the tab views, below the tabs.
children: _tabs.map((String name) {
return SafeArea(
top: false,
bottom: false,
child: Builder(
// This Builder is needed to provide a BuildContext that is "inside"
// the NestedScrollView, so that sliverOverlapAbsorberHandleFor() can
// find the NestedScrollView.
builder: (BuildContext context) {
return CustomScrollView(
// The "controller" and "primary" members should be left
// unset, so that the NestedScrollView can control this
// inner scroll view.
// If the "controller" property is set, then this scroll
// view will not be associated with the NestedScrollView.
// The PageStorageKey should be unique to this ScrollView;
// it allows the list to remember its scroll position when
// the tab view is not on the screen.
key: PageStorageKey<String>(name),
slivers: <Widget>[
SliverOverlapInjector(
// This is the flip side of the SliverOverlapAbsorber above.
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(
context),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
// In this example, the inner scroll view has
// fixed-height list items, hence the use of
// SliverFixedExtentList. However, one could use any
// sliver widget here, e.g. SliverList or SliverGrid.
sliver: SliverFixedExtentList(
// The items in this example are fixed to 48 pixels
// high. This matches the Material Design spec for
// ListTile widgets.
itemExtent: 60.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
// This builder is called for each child.
// In this example, we just number each list item.
return Container(
color: Color((math.Random().nextDouble() *
0xFFFFFF)
.toInt() <<
0)
.withOpacity(1.0));
},
// The childCount of the SliverChildBuilderDelegate
// specifies how many children this inner list
// has. In this example, each tab has a list of
// exactly 30 items, but this is arbitrary.
childCount: 30,
),
),
),
],
);
},
),
);
}).toList(),
),
),
),
),
);
}
通过将 SliverAppbar 与 NestedScrollView 结合使用,我能够使带有 Tabbar 的浮动 Appbar 类似于 WhatsApp。
在 NestedScrollView 和
中添加 floatHeaderSlivers: true固定:true,浮动:true,在 SliverAppBar
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: CustomSliverAppbar(),
);
}
}
class CustomSliverAppbar extends StatefulWidget {
@override
_CustomSliverAppbarState createState() => _CustomSliverAppbarState();
}
class _CustomSliverAppbarState extends State<CustomSliverAppbar>
with SingleTickerProviderStateMixin {
TabController _tabController;
@override
void initState() {
_tabController = TabController(
initialIndex: 0,
length: 2,
vsync: this,
);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
title: Text(
"WhatsApp type sliver appbar",
),
centerTitle: true,
pinned: true,
floating: true,
bottom: TabBar(
indicatorColor: Colors.black,
labelPadding: const EdgeInsets.only(
bottom: 16,
),
controller: _tabController,
tabs: [
Text("TAB A"),
Text("TAB B"),
]),
),
];
},
body: TabBarView(
controller: _tabController,
children: [
TabA(),
const Center(
child: Text('Display Tab 2',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
],
),
),
);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
}
class TabA extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scrollbar(
child: ListView.separated(
separatorBuilder: (context, child) => Divider(
height: 1,
),
padding: EdgeInsets.all(0.0),
itemCount: 30,
itemBuilder: (context, i) {
return Container(
height: 100,
width: double.infinity,
color: Colors.primaries[Random().nextInt(Colors.primaries.length)],
);
},
),
);
}
}
更新 - Sliver 应用栏扩展
如果你想看到 Sliver App Bar 在有人向上滚动时展开,即不是一直滚动到顶部而是一点点,那么只需将代码中的 snap: false
更改为 snap: true
: )
解决方案[修复所有点]
冲浪 google、Whosebug、github 问题、reddit 几个小时。我终于可以想出一个解决以下问题的解决方案:
Sliver 应用栏,标题被隐藏,向下滚动后只有标签栏可见。当你到达顶部时,你会再次看到标题。
主要:当您在选项卡 1 中滚动然后导航到选项卡 2 时,您不会看到任何重叠。 Tab 2的内容不会被Sliver App bar遮挡
List 中最顶部元素的 Sliver Padding 为 0。
在单个选项卡中保留滚动位置
下面是代码,我会尝试解释一下 (dartpad preview) :
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyStatelessWidget(),
);
}
}
class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final List<String> _tabs = <String>['Tab 1', 'Tab 2'];
return DefaultTabController(
length: _tabs.length,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: const Text('Books'),
floating: true,
pinned: true,
snap: false,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
tabs: _tabs.map((String name) => Tab(text: name)).toList(),
),
),
),
];
},
body: TabBarView(
children: _tabs.map((String name) {
return SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (BuildContext context) {
return CustomScrollView(
key: PageStorageKey<String>(name),
slivers: <Widget>[
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
);
},
childCount: 30,
),
),
),
],
);
},
),
);
}).toList(),
),
),
),
);
}
}
在 dartpad 中测试你想要的一切,一旦你没问题,那么让我们试着了解这里发生了什么。
大部分代码来自flutter documentation of NestedScrollView
他们在评论中提到得很好。我不是专家,所以我只强调我认为解决了大部分问题的方法。
我认为这里有两点很关键:
SliverOverlapAbsorber
&SliverOverlapInjector
- 使用
SliverList
代替ListView
无论我们看到什么额外的 space 或 space 应用栏消耗和第一个列表项重叠主要通过使用以上两点解决。
为了记住选项卡的滚动位置,他们在 CustomScrollView
:
PageStorageKey
key: PageStorageKey<String>(name),
name
只是一个字符串 -> 'Tab 1'
他们还在文档中提到我们可以使用 SliverFixedExtentList、SliverGrid,基本上是 Sliver
小部件。现在应该在需要时使用 Sliver 小部件。在一个 Flutter Youtube 视频(官方频道)中,他们提到 ListView、GridView 都是 Sliver 的高级实现。所以如果你想超级定制滚动或外观行为,Slivers 是低级的东西。
如果我遗漏了什么或说错了,请在评论中告诉我。