Flutter可滑动组件
1. ListView
1.1 ListView介绍
移动端数据量比较大时,一般都是通过列表来进行展示的,比如商品数据、聊天列表、通信录、朋友圈等。
在Android中,我们可以使用ListView或RecyclerView来实现,在Ios中,我们可以通过UITableView来实现。
在Flutter中,我们也有对应的列表Widget,就是ListView。
注意:在Flutter里面想要实现滑动效果,都需要在组件外部包裹滚动的视图。
1.2 默认构造函数
默认构造函数有一个children参数,它接受一个Widget列表(List<Widget>)。这种方式适合只有少量的子组件数量已知且比较少的情况。
代码语言:txt复制class ListViewDemo01 extends StatelessWidget {
const ListViewDemo01({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("莫听穿林打叶声", style: TextStyle(fontSize: 20, color: Colors.redAccent)),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("何妨吟啸且徐行", style: TextStyle(fontSize: 20, color: Colors.redAccent)),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生!", style: TextStyle(fontSize: 20, color: Colors.redAccent)),
)
],
);
}
}
效果图如下:
<center>
undefined
</center>
图中的空白是滑动ListView时所产生的效果。
注意:ListView的默认构造器适合子组件数量较少的时候使用。因为默认构造器中接收了一组明确的Widget,构造这组Widget时会一次性将所有子组件都初始化,而不是只初始化那些可见的Widget,即默认构造器不存在懒加载功能。
1.3 ListTile
在开发中,我们经常见到一种列表,有一个图标或图片(Icon),有一个标题(Title),有一个子标题(Subtitle),还有尾部一个图标(Icon)。
这个时候,我们可以使用ListTile来实现:
代码语言:txt复制class ListViewDemo02 extends StatelessWidget {
const ListViewDemo02({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
ListTile(
leading: Icon(Icons.people, size: 36,),
title: Text("联系人"),
subtitle: Text("联系人信息"),
trailing: Icon(Icons.arrow_forward_ios),
),
ListTile(
leading: Icon(Icons.email, size: 36,),
title: Text("邮箱"),
subtitle: Text("邮箱地址信息"),
trailing: Icon(Icons.arrow_forward_ios),
),
ListTile(
leading: Icon(Icons.message, size: 36,),
title: Text("消息"),
subtitle: Text("消息详情信息"),
trailing: Icon(Icons.arrow_forward_ios),
),
ListTile(
leading: Icon(Icons.map, size: 36,),
title: Text("地址"),
subtitle: Text("地址详情信息"),
trailing: Icon(Icons.arrow_forward_ios),
)
],
);
}
}
效果图如下:
<center>
undefined
</center>
1.4 ListView.builder()
除了默认构造函数外,ListView还提供了一些其他构造滑动视图的方法,先来看一下ListView.builder()。
源码分析:
代码语言:txt复制ListView.builder({
// ListView公共参数已省略
...
required IndexedWidgetBuilder itemBuilder,
int itemCount,
...
})
- itemBuilder:它是列表项的构建器,类型为IndexedWidgetBuilder,返回值为一个widget。当列表滚动到具体的index位置时,会调用该构建器构建列表项。
- itemCount:列表项的数量,如果为null,则为无限列表。
代码演示:
代码语言:txt复制class ListViewDemo03 extends StatelessWidget {
const ListViewDemo03({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.builder(
itemExtent: 50,
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("滑动列表演示$index"));
},
);
}
}
上面创建了容量为100的滑动列表,同时将每个item的高度强行设置为50。当子Widget即将被展示到屏幕中时,itemBuilder函数才会被调用。
效果图如下:
<center>
undefined
</center>
1.5 ListView.separated()
ListView.separated()的功能与ListView.builder()基本相同,它比ListView.builder()多了一个separatorBuilder参数,该参数是一个分割组件生成器。
代码演示:
代码语言:txt复制class ListViewDemo04 extends StatelessWidget {
const ListViewDemo04({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.separated(
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("滑动列表演示$index"));
},
separatorBuilder: (context, index) {
// 根据奇偶数设置不同颜色的分割线
return index%2==0? const Divider(color: Colors.blue, thickness: 10,) :
const Divider(color: Colors.deepOrange, thickness: 10,);
},
);
}
}
效果图如下:
<center>
undefined
</center>
1.6 ListView.custom()
开发者也可以使用ListView.custom()方法创建ListView,该方法中的childrenDelegate变量需要传入一个Sliver类。Sliver类会在后面有所介绍。
代码语言:txt复制ListView.custom({
// ListView公共参数已省略
...
required this.childrenDelegate,
...
})
2. GridView
2.1 GridView介绍
GridView常用于多行多列地展示,比如直播应用中的主播列表、电商中的商品列表等等。
在Flutter中我们可以使用GridView来实现,使用方式和ListView也比较相似。
2.2 默认构造函数
使用默认构造函数来创建GridView时,和ListView相比,需要传入一个特殊的参数:gridDelegate。
代码语言:txt复制 GridView({
// 省略
required this.gridDelegate,
List<Widget> children = const <Widget>[],
// 省略
})
该变量要求传入一个SliverGridDelegate对象,而该类为抽象类,有以下两个常用的子类。
代码语言:txt复制SliverGridDelegateWithFixedCrossAxisCount({
@required double crossAxisCount, // 交叉轴的item个数
double mainAxisSpacing = 0.0, // 主轴的间距
double crossAxisSpacing = 0.0, // 交叉轴的间距
double childAspectRatio = 1.0, // 子Widget的宽高比
})
SliverGridDelegateWithMaxCrossAxisExtent({
@required double maxCrossAxisExtent, // 每个item最大宽度
double mainAxisSpacing = 0.0, // 主轴的间距
double crossAxisSpacing = 0.0, // 交叉轴的间距
double childAspectRatio = 1.0, // 子Widget的宽高比
})
相比之下,SliverGridDelegateWithFixedCrossAxisCount更符合开发者的使用习惯,即可以直接指定交叉轴上的item个数。
而SliverGridDelegateWithMaxCrossAxisExtent的maxCrossAxisExtent属性虽然限定了每个item的最大宽度,但是横轴方向每个子元素的长度仍然是等分的,举个例子,如果横轴长度是450,那么当maxCrossAxisExtent的值在区间[450/4,450/3)内的话,子元素最终实际长度都为112.5。
代码演示:
代码语言:txt复制class GridViewDemo01 extends StatelessWidget {
const GridViewDemo01({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
),
children: List.generate(100, (index) {
return Container(
color: Colors.blue,
);
}
),
);
}
}
效果图如下:
<center>
undefined
</center>
2.3 GridView.count()
在上面使用默认构造器创建GridView时,需要传入gridDelegate对象。可以看到上面传入的类名很长,使用起来很不方便。GridView.count方法对SliverGridDelegateWithFixedCrossAxisCount进行了封装,方便开发者使用。
源码分析:
代码语言:txt复制GridView.count({
// 省略
required int crossAxisCount,
List<Widget> children = const <Widget>[],
// 省略
}) : gridDelegate = SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisSpacing: mainAxisSpacing,
crossAxisSpacing: crossAxisSpacing,
childAspectRatio: childAspectRatio,
),
从GridView.count()的源码里可以看到,开发者传入crossAxisCount后,同样是创建了SliverGridDelegateWithFixedCrossAxisCount对象的。
2.4 GridView.extent()
GridView.extent()方法对SliverGridDelegateWithMaxCrossAxisExtent进行了封装,方便开发者使用。
源码分析:
代码语言:txt复制GridView.extent({
// 省略
required double maxCrossAxisExtent,
List<Widget> children = const <Widget>[],
// 省略
}):
gridDelegate = SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: maxCrossAxisExtent,
mainAxisSpacing: mainAxisSpacing,
crossAxisSpacing: crossAxisSpacing,
childAspectRatio: childAspectRatio,
),
2.5 GridView.builder()
GridView.builder()方法与ListView相似,可以达到当view出现在手机屏幕时才进行加载的目的。
源码分析:
代码语言:txt复制GridView.builder({
// 省略
required double this.gridDelegate,
required IndexedWidgetBuilder itemBuilder,
// 省略
})
代码演示:
代码语言:txt复制class GridViewDemo03 extends StatelessWidget {
const GridViewDemo03({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return GridView.builder(
itemBuilder: (BuildContext context, int index) {
return Container(
color: Colors.red,
alignment: Alignment.center,
child: Text("GridView $index"),
);
},
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.5
),
);
}
}
效果图如下:
<center>
undefined
</center>
3.Sliver与CustomScrollView
3.1 可滚动组件剖析
Flutter 中的可滚动主要由三个角色组成:Scrollable、Viewport 和 Sliver:
- Scrollable :用于处理滑动手势,确定滑动偏移,滑动偏移变化时构建 Viewport 。
- Viewport:显示的视窗,即列表的可视区域;
- Sliver:视窗里显示的元素
前面介绍的 ListView、GridView都是一个完整的可滚动组件。如果我们想要在一个页面中,同时包含多个可滚动组件,且使它们的滑动效果能统一起来,比如一个滑动的视图中包括一个列表视图(ListView),一个网格视图(GridView),且让他们的滑动效果统一。
Flutter中有一个可以完成这样滚动效果的Widget:CustomScrollView,其成员变量slivers可以接收一组Sliver,从而达到统一管理多个滚动视图的目的。
3.2 Flutter 中常用的 Sliver
Sliver名称 | 功能 | 对应的可滚动组件 |
---|---|---|
SliverList | 列表 | ListView |
SliverFixedExtentList | 高度固定的列表 | 指定itemExtent的ListView |
SliverAnimatedList | 添加/删除列表项可以执行动画 | AnimatedList |
SliverGrid | 网格 | GridView |
SliverPrototypeExtentList | 根据原型生成高度固定的列表 | 指定prototypeItem的ListView |
SliverFillViewport | 包含多给子组件,每个都可以填满屏幕 | PageView |
除了和列表对应的 Sliver 之外还有一些用于对 Sliver 进行布局、装饰的组件,它们的子组件必须是 Sliver,我们列举几个常用的:
Sliver名称 | 对应 RenderBox |
---|---|
SliverPadding | Padding |
SliverVisibility、SliverOpacity | Visibility、Opacity |
SliverFadeTransition | FadeTransition |
SliverLayoutBuilder | LayoutBuilder |
Sliver系列 Widget 比较多,需要时再去查看文档即可。
上面提及的部分组件是和可滚动组件无关的,它们主要是为了结合CustomScrollView一起使用,这是因为CustomScrollView的子组件必须都是Sliver。
3.3 Sliver的基本使用
简单演示一下:SliverGrid SliverPadding SliverSafeArea的组合。
SliverGrid用来实现网格效果,SliverPadding用来进行填充,SliverSafeArea设置内容显示在安全区域(比如不让齐刘海挡住我们的内容)。
代码语言:txt复制class CustomScrollViewDemo01 extends StatelessWidget {
const CustomScrollViewDemo01({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverSafeArea(
sliver: SliverPadding(
padding: EdgeInsets.all(8),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 2,
mainAxisSpacing: 2,
childAspectRatio: 1.5
),
delegate: SliverChildBuilderDelegate(
(context,index) {
return Container(
alignment: Alignment.center,
color: Colors.red,
child: Text("item $index"),
);
},
childCount: 20
),
),
)
)
],
);
}
}
效果图如下:
<center>
undefined
</center>
3.4 Sliver的组合使用
这里使用官方的示例程序,将SliverAppBar SliverGrid SliverFixedExtentList做出如下界面:
代码语言:txt复制class HomeContent extends StatelessWidget {
const HomeContent({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 250,
flexibleSpace: FlexibleSpaceBar(
title: Text("Sliver组合使用"),
background:
Image.network(
"https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg",
fit: BoxFit.cover,
),
),
),
SliverGrid(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Container(
alignment: Alignment.center,
color: Colors.teal[100 * (index%9)],
child: Text("frid item $index"),
);
},
childCount: 10
),
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 4
)
),
SliverFixedExtentList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: Text("list item $index"),
);
},
childCount: 20
),
itemExtent: 50
)
],
);
}
}
效果图如下:
<center>
undefined
</center>
3.5 Sliver懒加载的原理
在上面讲解ListView.builder() 与 GridView.builder() 时提到该方法创造出的可滑动组件可以对其中显示的内容实现懒加载。
此处使用ListView作为示例进行讲解。Flutter官方文档中提到,ListView的默认构造器建议在需要展示的元素个数较少时使用,在展示的元素数量较多时,建议使用ListView.builder() 方法构造视图。
深入查看ListView的源码后可以发现,在默认构造器中使用了SliverChildListDelegate类创建了一个成员变量,而在构造方法中传入的children即作为创建该对象的入参。
代码语言:txt复制childrenDelegate = SliverChildListDelegate(
children, // ListView默认构造器中接收的Widget数组透传
// 代码省略
),
而ListView.builder() 方法,将传入的itemBuilder函数透传,使用SliverChildBuilderDelegate类创建了对象。
代码语言:txt复制childrenDelegate = SliverChildBuilderDelegate(
itemBuilder, // ListView.builder() 中的itemBuilder函数透传
// 代码省略
)
而SliverChildListDelegate与SliverChildBuilderDelegate都是继承自SliverChildDelegate,经过仔细阅读代码,发现具体的细节就在父类SliverChildDelegate的build方法中,该方法是抽象方法,具体逻辑需要子类去实现。
下面是SliverChildListDelegate与SliverChildBuilderDelegate两个类对build的实现。
代码语言:txt复制SliverChildListDelegate:
build(BuildContext context, int index) {
// 代码省略
Widget child = children[index];
}
SliverChildBuilderDelegate:
build(BuildContext context, int index) {
// 代码省略
child = builder(context, index);
}
从代码中可以清楚地看到,SliverChildListDelegate的实现,是直接从传入的list中取出index对应下标的Widget,而SliverChildBuilderDelegate的实现是根据传入的index实时创建。
之后则需要验证当view进入手机可视区域后,通过传入index参数调用SliverChildDelegate.build()方法,从而创建出对应的视图。
而SliverChildDelegate中build方法调用链则比较冗长,大概顺序如下 :
代码语言:txt复制SliverChildDelegate.build() <=
SliverMultiBoxAdaptorElement._build() <= createChild() <=
RenderSlierMultiBoxAdaptor._createOrObtainChild() <=
RenderSliverList.performLayout() <=
RenderObject.performLayout()
整体的调用逻辑如上面所示,感兴趣的读者可以借助以上调用链阅读源码。
对可滑动组件懒加载的原理进行简单归纳后,可总结如下:
SliverChildListDelegate中children是在创建视图是传入的一组明确的Widget,在展示前这组Widget便已存在;而SliverChildBuilderDelegate则使用传入的itemBuilder函数通过index参数实时创建Widget。
通过以上的分析可知,若CustomScrollView中的Sliver用到了SliverChildBuilderDelegate,则此CustomScrollView也会包含懒加载的特性。
4.滚动事件监听
4.1 滚动事件监听介绍
对于滚动的视图,我们经常需要监听它的一些滚动事件,在监听到滚动事件时执行对应的操作。
比如视图滚动到底部时,我们可能希望做上拉加载更多;比如滚动到一定位置时显示一个回到顶部的按钮,点击回到顶部的按钮,回到顶部;比如监听滚动什么时候开始,什么时候结束;
在Flutter中监听滚动相关的内容由两部分组成:ScrollController和NotificationListener。
4.2 ScrollController
在Flutter中,Widget并不是最终渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常这种监听事件以及相关的信息并不能直接从Widget中获取,而是必须通过对应的Widget的Controller来实现。
ListView、GridView的组件控制器是ScrollController,我们可以通过它来获取视图的滚动信息,并且可以调用里面的方法来更新视图的滚动位置。
下面使用功能演示来学习ScrollController的使用。当滚动到1000位置的时候,显示一个回到顶部的按钮:
代码语言:txt复制class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// 成员变量初始化ScrollController,initialScrollOffset参数可以指定可滑动视图的初始位置。
ScrollController controller = ScrollController(initialScrollOffset: 300);
// 标志位记录是否需要显示浮动按钮
bool isShowFloatingButton = false;
@override
void initState() {
super.initState();
// 类初始化时,向ScrollController添加监听
controller.addListener(() {
// 位置偏移量可以通过controller.offset获取
print("监听到滚动: ${controller.offset}");
setState(() {
// 发生滑动时,判断当前位置是否大大于1000,大于1000时需要显示浮动按钮
isShowFloatingButton = controller.offset >=1000;
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ScrollController测试'),
),
body: ListView.builder(
controller: controller,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.people),
title: Text("联系人$index"),
);
}
),
// 需要显示浮动按钮时则显示,否则浮动按钮置为null
floatingActionButton: isShowFloatingButton ? FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
// 点击按钮时,1s动画回到初始0位置
controller.animateTo(0, duration: Duration(seconds: 1), curve: Curves.easeIn);
},
) : null,
);
}
}
效果图如下:
<center>
undefined
</center>
其中有以下几点需要进行说明:
- ScrollController有以下两个方法:jumpTo(double offset)、animateTo(double offset,...),这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。
- ScrollController可以通过initialScrollOffset设置初始位置,也可以监听到滚动的位置,但无法监听到开始滚动与结束滚动的事件。
4.3 NotificationListener
如果我们希望监听什么时候开始滚动,什么时候结束滚动,这个时候我们可以通过NotificationListener这个组件实现。
- NotificationListener是一个Widget,模板参数T是想监听的通知类型,如果省略,则所有类型通知都会被监听,如果指定特定类型,则只有该类型的通知会被监听。
- NotificationListener需要一个onNotification回调函数,用于实现监听处理逻辑。
- 该回调可以返回一个布尔值,代表是否阻止该事件继续向上冒泡,如果为true时,则冒泡终止,事件停止向上传播,如果不返回或者返回值为false 时,则冒泡继续。
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('NotificationListener测试'),
),
body: NotificationListener(
onNotification: (ScrollNotification scrollNotification) {
if(scrollNotification is ScrollStartNotification) {
print("开始滚动");
} else if (scrollNotification is ScrollUpdateNotification) {
print("正在滚动:${scrollNotification.metrics.pixels}");
} else if (scrollNotification is ScrollEndNotification) {
print("结束滚动");
}
return false;
},
child: ListView.builder(
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.people),
title: Text("联系人$index"),
);
}
),
),
);
}
}
效果图如下:
<center>
undefined
</center>
5. TabBarView
5.1 TabBarView介绍
源码分析:
代码语言:txt复制 TabBarView({
Key? key,
required this.children, // tab 页
this.controller, // TabController
this.physics,
this.dragStartBehavior = DragStartBehavior.start,
})
TabController 用于监听和控制 TabBarView 的页面切换,通常和 TabBar 联动。如果没有指定,则会在组件树中向上查找并使用最近的一个 DefaultTabController 。
DefaultTabController是一个Widget组件,后面示例中可以看到如何对其进行使用。
5.2 TabBar介绍
源码分析:
代码语言:txt复制const TabBar({
Key? key,
required this.tabs, // 具体的 Tabs,需要我们创建
this.controller,
this.isScrollable = false, // 是否可以滑动
this.padding,
this.indicatorColor,// 指示器颜色,默认是高度为2的一条下划线
this.automaticIndicatorColorAdjustment = true,
this.indicatorWeight = 2.0,// 指示器高度
this.indicatorPadding = EdgeInsets.zero, //指示器padding
this.indicator, // 指示器
this.indicatorSize, // 指示器长度,有两个可选值,一个tab的长度,一个是label长度
this.labelColor,
this.labelStyle,
this.labelPadding,
this.unselectedLabelColor,
this.unselectedLabelStyle,
this.mouseCursor,
this.onTap,
...
})
效果图如下:
<center>
undefined
</center>
TabBar 通常位于 AppBar 的底部,它也可以接收一个 TabController ,如果需要和 TabBarView 联动, TabBar 和 TabBarView 使用同一个 TabController 即可,注意,联动时 TabBar 和 TabBarView 的child数量需要一致。如果没有指定 controller,则会在组件树中向上查找并使用最近的一个 DefaultTabController 。
TabBar的 tabs 参数 接收的是,tab 可以是任何 Widget,不过Material 组件库中已经实现了一个 Tab 组件,我们一般都会直接使用它:
代码语言:txt复制const Tab({
Key? key,
this.text, //文本
this.icon, // 图标
this.iconMargin = const EdgeInsets.only(bottom: 10.0),
this.height,
this.child, // 自定义 widget
})
5.3 TabController使用
TabBar位于AppBar的底部,而TabBarView位于界面展示的body中,想要在滑动TabBarView时,TabBar同样跟着滑动,则可以通过TabController实现。
代码语言:txt复制class _HomePageState extends State<HomePage> {
// late关键字可以让关键字懒加载
late TabController _tabController;
@override
void initState() {
super.initState();
// TabBar与TabBarVIew的length的长度需保持一致。
// ScrollableState() 表明需同步滑动状态
_tabController = TabController(length: 3, vsync: ScrollableState());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('TabBarView测试'),
bottom: TabBar(
controller: _tabController,
tabs: [
Tab(text: "新闻"),
Tab(text: "历史"),
Tab(text: "图片"),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
Container(
alignment: Alignment.center,
child: Text("新闻", textScaleFactor: 5),
),
Container(
alignment: Alignment.center,
child: Text("历史", textScaleFactor: 5),
),
Container(
alignment: Alignment.center,
child: Text("图片", textScaleFactor: 5),
),
],
),
);
}
}
效果图如下:
<center>
undefined
</center>
5.4 DefaultTabController使用
使用TabBar和TabBarView时若没有指定 controller,则会在组件树中向上查找并使用最近的一个 DefaultTabController 。
DefaultTabController是一个Widget,下面是对应的示例代码:
代码语言:txt复制class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: Text("TabBarView测试"),
bottom: TabBar(
tabs: [
Tab(text: "新闻"),
Tab(text: "历史"),
Tab(text: "图片"),
],
),
),
body: TabBarView(
children: [
Container(
alignment: Alignment.center,
child: Text("新闻", textScaleFactor: 5),
),
Container(
alignment: Alignment.center,
child: Text("历史", textScaleFactor: 5),
),
Container(
alignment: Alignment.center,
child: Text("图片", textScaleFactor: 5),
),
],
),
)
);
}
}
效果图如下:
<center>
undefined
</center>
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined