在 Android 中,如果 ScrollView 包含的一个 RecyclerView,则会造成滑动冲突,需要我们自行处理。
而在 Flutter 中,为我们提供了 NestedScrollView
来帮助我们解决多个滑动 Widget 嵌套的问题。
NestedScrollView
通过为外部 ScrollView
和内部 ScrollView
提供自定义 ScrollController
来解决此问题,将它们链接在一起,以便它们作为一个连贯的滚动视图显示给用户。
构造方法
const NestedScrollView({
Key key,
this.controller,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.physics,
@required this.headerSliverBuilder,
@required this.body,
this.dragStartBehavior = DragStartBehavior.start,
})
controller,控制器,控制 Widget 的滚动
scrollDirection,设置视图的滚动方向
reverse,是否按照阅读方向相反的方向滑动,设置水平滚动时
设置水平滚动时
- 若
reverse: false
时,则滚动内容头部和左侧对其, 那么滑动方向就是从左向右 - 若
reverse: true
时,则滚动内容尾部和右侧对其, 那么滑动方向就是从右往左。
其实此属性本质上是决定可滚动
widget
的初始滚动位置是在头还是尾,取false
时,初始滚动位置在头,反之则在尾- 若
physics,接受一个
ScrollPhysics
对象,它决定可滚动Widget
如何响应用户操作,比如用户滑动完抬起手指后,继续执行动画;或者滑动到边界时,如何显示。默认情况下,Flutter 会根据具体平台分别使用不同的
ScrollPhysics
对象,应用不同的显示效果,如当滑动到边界时,继续拖动的话,在iOS
上会出现弹性效果,而在Android
上会出现微光效果。如果你想在所有平台下使用同一种效果,可以显式指定,Flutter SDK 中包含了两个
ScrollPhysics
的子类可以直接使用:ClampingScrollPhysics
:安卓下微光效果。BouncingScrollPhysics
:iOS
下弹性效果。
headerSliverBuilder,通常是带有
TabBar
的SliverAppBar
。body,主要部分,通常是一个 TabBarView,显示的主体部分
dragStartBehavior,确定处理拖动开始行为的方式。
示例
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Text Demo',
theme: ThemeData(primarySwatch: Colors.green),
home: MyHomePage(title: 'Text 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> {
Choice _selectedChoice = choices[0]; // The app's "state".
void _select(Choice choice) {
setState(() {
// Causes the app to rebuild with the new _selectedChoice.
_selectedChoice = choice;
});
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: choices.length,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: _headerSliverBuilder,
body: TabBarView(
children: choices.map((Choice choice) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: ChoiceCard(choice: choice),
);
}).toList(),
),
),
),
);
}
List<Widget> _headerSliverBuilder(
BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
//1.在标题左侧显示的一个控件,在首页通常显示应用的 logo;在其他界面通常显示为返回按钮
leading: Icon(_selectedChoice.icon),
//2. ? 控制是否应该尝试暗示前导小部件为null
automaticallyImplyLeading: true,
//3.当前界面的标题文字
title: Text(_selectedChoice.title),
//4.一个 Widget 列表,代表 Toolbar 中所显示的菜单,对于常用的菜单,通常使用 IconButton 来表示;
//对于不常用的菜单通常使用 PopupMenuButton 来显示为三个点,点击后弹出二级菜单
actions: <Widget>[
IconButton(
// action button
icon: Icon(choices[0].icon),
onPressed: () {
_select(choices[0]);
},
),
IconButton(
// action button
icon: Icon(choices[1].icon),
onPressed: () {
_select(choices[1]);
},
),
PopupMenuButton<Choice>(
// overflow menu
onSelected: _select,
itemBuilder: (BuildContext context) {
return choices.skip(2).map((Choice choice) {
return PopupMenuItem<Choice>(
value: choice,
child: Text(choice.title),
);
}).toList();
},
)
],
//5.一个显示在 AppBar 下方的控件,高度和 AppBar 高度一样,
// 可以实现一些特殊的效果,该属性通常在 SliverAppBar 中使用
flexibleSpace: FlexibleSpaceBar(
centerTitle: true,
background: Image(
image: AssetImage(
"assets/images/girl.png"),
fit: BoxFit.cover,
),
),
//6.一个 AppBarBottomWidget 对象,通常是 TabBar。用来在 Toolbar 标题下面显示一个 Tab 导航栏
bottom: TabBar(
isScrollable: true,
tabs: choices.map(
(Choice choice) {
return Tab(
text: choice.title,
icon: Icon(choice.icon),
);
},
).toList(),
),
//7.? 材料设计中控件的 z 坐标顺序,默认值为 4,对于可滚动的 SliverAppBar,
// 当 SliverAppBar 和内容同级的时候,该值为 0, 当内容滚动 SliverAppBar 变为 Toolbar 的时候,修改 elevation 的值
elevation: 1,
//APP bar 的颜色,默认值为 ThemeData.primaryColor。改值通常和下面的三个属性一起使用
backgroundColor: Colors.red,
//App bar 的亮度,有白色和黑色两种主题,默认值为 ThemeData.primaryColorBrightness
brightness: Brightness.light,
//App bar 上图标的颜色、透明度、和尺寸信息。默认值为 ThemeData().primaryIconTheme
iconTheme: ThemeData().primaryIconTheme,
//App bar 上的文字主题。默认值为 ThemeData().primaryTextTheme
textTheme: ThemeData().accentTextTheme,
//此应用栏是否显示在屏幕顶部
primary: true,
//标题是否居中显示,默认值根据不同的操作系统,显示方式不一样,true居中 false居左
centerTitle: true,
//横轴上标题内容 周围的间距
titleSpacing: NavigationToolbar.kMiddleSpacing,
//展开高度
expandedHeight: 200,
//是否随着滑动隐藏标题
floating: true,
//tab 是否固定在顶部
pinned: true,
//与floating结合使用
snap: true,
)
];
}
}
class Choice {
const Choice({this.title, this.icon});
final String title;
final IconData icon;
}
const List<Choice> choices = const <Choice>[
const Choice(title: 'Car', icon: Icons.directions_car),
const Choice(title: 'Bicycle', icon: Icons.directions_bike),
const Choice(title: 'Boat', icon: Icons.directions_boat),
const Choice(title: 'Bus', icon: Icons.directions_bus),
const Choice(title: 'Train', icon: Icons.directions_railway),
const Choice(title: 'Walk', icon: Icons.directions_walk),
];
class ChoiceCard extends StatelessWidget {
const ChoiceCard({Key key, this.choice}) : super(key: key);
final Choice choice;
@override
Widget build(BuildContext context) {
final TextStyle textStyle = Theme.of(context).textTheme.display1;
Widget _itemBuilder(BuildContext context, int index) {
return ListTile(
leading: Icon(choice.icon),
title: Text("this is a " + choice.title),
);
}
return Card(
color: Colors.white,
child: Center(
child: ListView.builder(
itemBuilder: _itemBuilder,
itemCount: 30,
),
),
);
}
}
效果如下: