网易首页 > 网易号 > 正文 申请入驻

Flutter 入门指北之滑动部件(超详细)

0
分享至

  码个蛋(codeegg)第 603 次推文

  前言

  Flutter系列文章:

  《Flutter 入门指北(Part 1)之 Dart》

  《Flutter 入门指北(Part 2)之基础部件》

  《Flutter 入门指北(Part 3)之 Appbar,Scaffold 填坑》

  《Flutter 入门指北(Part 4)之容器部件》

  《Flutter 入门指北(Part 5)之输入处理及实战》

  《Flutter 入门指北(Part 6) 之路由》

  前面的小节基本上讲完了常用的部件和容器部件,也可以完成很多的界面,但是又一个问题,假如我们要显示一段文字,比如将一段又臭又长的文字在界面上显示 1000 次,不难完成吧

  // ..省略一些无关代码 body: Text('一段又臭又长的文字' * 1000, softWrap: true)

  很简单,运行到手机...「诶诶诶,**,怎么只显示了一部分,剩下的怎么画不下去」

  日常开发中,会遇到很多这种情况,许多界面不是一页就能够显示完的。那么这里提下可滑动的容器部件

  SingleChildScrollView

  这个部件非常简单,不贴源码了。最简单的使用方式只需要提供一个 child 即可。现在给前面写的 Text 包裹上一层 SingleChildScrollView 然后再运行,文字全部都展示出来了。

  如果需要实现一个垂直的滚动列表,可以直接通过 SingleChildScrollView 包裹 Column 来实现,列表内容全部塞到 Column 即可

  class SingleChildScrollDemoPage extends StatelessWidget { @override Widget build(BuildContext context) { /// letters 自由发挥吧...一定要大量,大量,大量 List letters = [......];
return Scaffold( appBar: AppBar( title: Text('Single Child Demo'), ), body: SingleChildScrollView( child: Center( child: Column( children: List.generate( letters.length, (index) => Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Text(letters[index], style: TextStyle(fontSize: 18.0)), )), ), )), ); } }

  运行结果会根据你的letters不同而不同,这边就不贴效果图了,反正你可以看到一串列表...

  那么如果需要实现横向滚动列表呢,稍稍做下修改就行了

  body: SingleChildScrollView( // 设置滚动方向 scrollDirection: Axis.horizontal, child: Center( // 修改为 `Row` 即可 child: Row( children: List.generate( letters.length, // 如果你的 letters 数量比较少,推荐加个 `Container` 把宽度指定大点 (index) => Container( child: Padding( padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 6.0), child: Text(letters[index], style: TextStyle(fontSize: 18.0)), ), width: 30.0)), ), ))

  效果图也不贴了,都比较简单。

  该部分代码查看single_child_scroll_main.dart文件*

  ListView

  平时开发 Android 的时候,如果有相同格式的列表要实现,一般会使用 ListView 或者 RecyclerView 来实现,Flutter 也提供了类似的部件 ListView

  实现 ListView 的方法主要有

  通过 ListView 设置 children 属性实现

  通过 ListView.custom 实现

  通过 ListView.builder 实现

  通过 ListView.separated 实现带分割线列表

  ListView children

  第一种方法实现列表,和通过 SingleChildScrollView + Column / Row 的方法比较类似,不过可以直接通过指定 ListView 的 scrollDirection 就可以了。

  body: ListView( // 通过修改滑动方向设置水平或者垂直方向滚动 scrollDirection: Axis.vertical, // 通过 iterable.map().toList 和 List.generate 方法效果是一样的 children: letters .map((s) => Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Center( child: Text(s)))) .toList()),

  ListView.custom

  body: ListView.custom( // 指定 item 的高度,可以加快渲染的速度 itemExtent: 40.0, // item 代理 childrenDelegate: SliverChildBuilderDelegate( // IndexedWidgetBuilder,根据 index 设置 item 中需要变化的数据 (_, index) => Center(child: Text(letters[index], style: TextStyle(color: Colors.red))), // 指定 item 的数量 childCount: letters.length, )),

  如果每个 item 的高度可以确定,那么推荐通过 itemExtent 来设置 item 的高度/宽度,能够加快 ListView 的渲染速度。如果不指定高度/宽度,ListView 需要根据每个 item 来计算 ListView 的高度,这个计算过程是需要消耗时间和资源的

  ListView.builder

  该方法同custom类似,custom需要通过一个Delegate生成item,该方法直接通过builder生成,同时也可以直接指定item的高度

  body: ListView.builder( itemBuilder: (_, index) => Center(child: Text(letters[index], style: TextStyle(color: Colors.green))), itemExtent: 40.0, itemCount: letters.length),

  相对比较简单,代码也比较少...就冲这点,我也愿意用这个方法

  ListView.separated

  如果需要在每个 item 之间添加分割线,那么通过以上的方式实现就比较困难了,所以 Flutter 提供了 separated 方法用来快速构建带有分割线的 ListView

  加入我们的 item 之间的分割线需要如下样式:奇数位和偶数位之间用黑色分割线,偶数位和奇数位之间用红色分割线

  // 需要分割线的时候才使用,不能指定 item 的高度 body: ListView.separated( itemBuilder: (_, index) => Padding( padding: const EdgeInsets.symmetric(vertical: 20.0), child: Center(child: Text(letters[index], style: TextStyle(color: Colors.blue))), ), // 这里用来定义分割线 separatorBuilder: (_, index) => Divider(height: 1.0, color: index % 2 == 0 ? Colors.black : Colors.red), itemCount: letters.length),

  最终的效果如下:

  以上代码查看listview_main.dart文件

  总结下:如果 item 的高度能够准确获取,一定要指定 itemExtent 的值,这样会更加高效,至于要通过哪种方式来生成,完全看个人喜好吧。

  ExpansionTile

  既然讲到了ListView,在日常开发中,折叠列表也是一个比较常用的,所以这边要提下ExpansionTile这个部件,因为相对比较简单,所以直接上代码了

  class ExpansionTilesDemoPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('ExpansionTile Demo'), ), body: ExpansionTile( // 最前面的 widget leading: Icon(Icons.phone_android), // 替换默认箭头 // trailing: Icon(Icons.phone_iphone), title: Text('Parent'), // 默认是否展开 initiallyExpanded: true, // 展开时候的背景色 backgroundColor: Colors.yellow[100], // 展开或者收缩的回调,true 表示展开 onExpansionChanged: (expanded) => print('ExpansionTile is ${expanded ? 'expanded' : 'collapsed'}'), children: List.generate( 10, (position) => Container( padding: const EdgeInsets.only(left: 80.0), child: Text('Children ${position + 1}'), height: 50.0, alignment: Alignment.centerLeft, )), ), ); } }

  这样就完成了一个折叠部件,看下最后的效果

  那么实现折叠列表也就是通过ListView创建一个ExpansionTile列表即可,先准备下模拟的数据

  final _keys = ['ParentA', 'ParentB', 'ParentC', 'ParentD', 'ParentE', 'ParentF']; final Map> _data = { 'ParentA': ['Child A0', 'Child A1', 'Child A2', 'Child A3', 'Child A4', 'Child A5'], 'ParentB': ['Child B0', 'Child B1', 'Child B2', 'Child B3', 'Child B4', 'Child B5'], 'ParentC': ['Child C0', 'Child C1', 'Child C2', 'Child C3', 'Child C4', 'Child C5'], 'ParentD': ['Child D0', 'Child D1', 'Child D2', 'Child D3', 'Child D4', 'Child D5'], 'ParentE': ['Child E0', 'Child E1', 'Child E2', 'Child E3', 'Child E4', 'Child E5'], 'ParentF': ['Child F0', 'Child F1', 'Child F2', 'Child F3', 'Child F4', 'Child F5'] };

  在平时开发过程中,后台返回的数据应该是列表嵌套列表的形式比较多,我这边主要就是为了偷懒就随便弄了,接着修改下body的代码

  body: ListView( children: _keys .map((key) => ExpansionTile( title: Text(key), children: _data[key] .map((value) => InkWell( child: Container( child: Text(value), padding: const EdgeInsets.only(left: 80.0), height: 50.0, alignment: Alignment.centerLeft, ), onTap: () {})) .toList(), )) .toList()),

  最终的效果就是个折叠列表了

  该部分代码查看expansion_tile_main.dart文件

  当然了,只要数据到位,别说两层折叠,三层,四层甚至更多层都能够实现,源码中有实现四层的demo,这边就不贴代码了,有需要的小伙伴可以查看源码

  GridView

  生成列表可以通过ListView来实现,那么同样,实现网格列表Flutter也提供了GridView来实现,实现GridView的方法也很多...我数了下,大概有 10 种..对你没看错,就是那么多,(诶诶诶,别走啊...虽然方法有点多,但是,大同小异)

  GridView

  GridView 需要一个 gridDelegate,gridDelegate 目前有两种

  SliverGridDelegateWithFixedCrossAxisCount 看命名就知道,值固定数量的,这个数量是只单排的数量

  SliverGridDelegateWithMaxCrossAxisExtent 这个是设置最大宽度/高度,在这个值范围内取最大值,比如一排能给你排下 6 个,但是远不到设置的最大值,它绝不给你排 6 个

  那么接下来的使用就比较简单了

  class GridViewDemoPage extends StatelessWidget { // 自行设置 final List letters = [ ..... ];
// 用于区分网格单元 final List colors = [Colors.red, Colors.green, Colors.blue, Colors.pink];
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('GridView Demo'), ), body: GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 5, // 单行的个数 mainAxisSpacing: 10.0, // 同 scrollDirection 挂钩,item 之间在主轴方向的间隔 crossAxisSpacing: 10.0, // item 之间在副轴方法的间隔 childAspectRatio: 1.0 // item 的宽高比 ), // 需要根据 index 设置不同背景色,所以使用 List.generate,如果不设置背景色,也可用 iterable.map().toList children: List.generate( letters.length, (index) => Container( alignment: Alignment.center, child: Text(letters[index]), color: colors[index % 4], )), ), ); } }

  关键地方已经添加了注释,跑下运行效果

  接下来换一种delegate试试效果,当然这个最大值可以根据个人喜好来设置

   body: GridView( // 通过设置 `maxCrossAxisExtent` 来指定最大的宽度,在这个值范围内,会选取相对较大的值 gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 60.0, crossAxisSpacing: 10.0, mainAxisSpacing: 10.0, childAspectRatio: 1.0), children: List.generate( letters.length, (index) => Container( alignment: Alignment.center, child: Text(letters[index]), color: colors[index % 4], )), )

  最后效果:

  为了方便写法呢,Flutter对以上的两种方式进行了封装,省略了delegate

  GridView.count/GridView.extent

  直接看下如何修改

  // 这种情况简化了 `GridView` 使用 `SliverGridDelegateWithFixedCrossAxisCount` 代理的方法 body: GridView.count( crossAxisSpacing: 10.0, mainAxisSpacing: 10.0, childAspectRatio: 1.0, crossAxisCount: 5, childAspectRatio: 2.0, children: List.generate( letters.length, (index) => Container( alignment: Alignment.center, color: colors[index % 4], child: Text(letters[index]), ))),

   // 这种情况简化了 `GridView` 使用 `SliverGridDelegateWithMaxCrossAxisExtent` 代理的方法 body: GridView.extent( crossAxisSpacing: 10.0, mainAxisSpacing: 10.0, childAspectRatio: 1.0, maxCrossAxisExtent: 60.0, children: List.generate( letters.length, (index) => Container( alignment: Alignment.center, color: colors[index % 4], child: Text(letters[index]), ))),

  运行的效果入和前面的相同

  GridView.custom

  这种生成方式,比 GridView 多了一个 childrenDelegate,childrenDelegate 主要分为两种,一种是通过 IndexedWidgetBuilder 来构建 item 的 SliverChildBuilderDelegate,还有一种是通过 List 来构建 item 的 SliverChildListDelegate,所以...这边直接有 4 中生成方式,当然,我们只需要了解 childrenDelegate 如何使用即可

  body: GridView.custom( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 5, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, childAspectRatio: 1.0), // item 通过 delegate 来生成,内部实现还是 `IndexedWidgetBuilder` childrenDelegate: SliverChildBuilderDelegate( (_, index) => Container( alignment: Alignment.center, color: colors[index % 4], child: Text(letters[index]), ), childCount: letters.length)),

   body: GridView.custom( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 5, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, childAspectRatio: 1.0), // 内部通过返回控件列表实现 childrenDelegate: SliverChildListDelegate( List.generate( letters.length, (index) => Container( child: Text(letters[index]), alignment: Alignment.center, color: colors[index % 4], )), )),

  运行效果也同上面。

  GridView.builder

  前面介绍的方法中,生成 item 的方式基本上是通过 List 进行转换的,在 custom 提到了 IndexWidgetBuilder 的生成方式,当然,在 ListView 的时候也用到了这种生成方式,当然 GridView 也有啊,要「雨露均沾」你说是吧

  //通过`IndexedWidgetBuilder`来构建item,别的参数同上 body: GridView.builder( // 这里又需要分两种 `gridDelegate` gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 5, crossAxisSpacing: 10.0, mainAxisSpacing: 10.0, childAspectRatio: 1.0), itemCount: letters.length, itemBuilder: (_, index) => Container(color: colors[index % 4], child: Text(letters[index]), alignment: Alignment.center)),

  到这 10 种方式就说完了。终于可以歇一口气了。

  该部分代码查看gridview_main.dart文件

  CustomScrollView

  在平时的开发中,应该会遇到这么种情况,头部是一个GridView接下来拼接一些别的部件,然后再拼接一个列表,例如下图

  因为 GridView 和 ListView 亮着都是可滑动的部件,直接拼接肯定会有「滑动冲突」,所以 Flutter 就提供了一个粘合剂,CustomScrollView,那么 Flutter 如何实现呢,因为会涉及到 Sliver 系列部件,所以这边先看下大概的代码,下节会补充 Sliver 系列部件的内容

  class CustomScrollDemoPage extends StatelessWidget { // 这边用的 A-Z 字母 final List letters = [ ..... ];
final List colors = [Colors.red, Colors.green, Colors.blue, Colors.pink, Colors.yellow, Colors.deepPurple];
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('CustomScrollDemo'), ), body: CustomScrollView( // 这里需要传入 `Sliver` 部件,下节课填坑 slivers: [ // SliverGrid 实现同 GridView 实现方式一样 // 同样 SliverGrid 有提供 `count`, `entent` 方法便于快速生成 SliverGrid SliverGrid( delegate: SliverChildBuilderDelegate( (_, index) => InkWell( child: Image.asset('images/ali.jpg'), onTap: () {}, ), childCount: 8), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0)), // 这里下节讲 SliverToBoxAdapter( child: Container( color: Colors.black12, margin: const EdgeInsets.symmetric(vertical: 10.0), child: Column(children: [ Divider(height: 2.0, color: Colors.black54), Stack( alignment: Alignment.center, children: [ Image.asset('images/app_bar_hor.jpg', fit: BoxFit.cover), Text('我是一些别的东西..例如广告', textScaleFactor: 1.5, style: TextStyle(color: Colors.red)) ], ), Divider(height: 2.0, color: Colors.black54), ], mainAxisAlignment: MainAxisAlignment.spaceBetween), alignment: Alignment.center)), // SliverFixedExtentList 实现同 List.custom 实现类似 SliverFixedExtentList( delegate: SliverChildBuilderDelegate( (_, index) => InkWell( child: Container( child: Text(letters[index] * 10, style: TextStyle(color: colors[index % colors.length], letterSpacing: 2.0), textScaleFactor: 1.5), alignment: Alignment.center, ), onTap: () {}, ), childCount: letters.length), itemExtent: 60.0) ], ), ); } }

  该部分代码查看 custom_scroll_main.dart 文件

  滑动部件其实还有好几个,但是以上介绍的在平时开发过程中够用了,如果后期发现还需要别的部件,我会继续补上。在结束前,我们再说下如何通过 ScrollController 来控制 Scrollable 的滚动位置。例如我们需要实现,当滚动的距离大于一定距离的时候显示一个回到顶部的按钮,有了 ScrollController 就能够非常方便的实现

  ScrollController

  因为需要根据滑动的距离显示回到顶部按钮,那么就需要通过一个状态位来控制按钮显隐

  class ScrollControllerDemoPage extends StatefulWidget { @override _ScrollControllerDemoPageState createState() => _ScrollControllerDemoPageState(); }
class _ScrollControllerDemoPageState extends State { var _scrollController = ScrollController(); var _showBackTop = false;
@override void initState() { super.initState();
// 对 scrollController 进行监听 _scrollController.addListener(() { // _scrollController.position.pixels 获取当前滚动部件滚动的距离 // window.physicalSize.height 获取屏幕高度 // 当滚动距离大于 800 后,显示回到顶部按钮 setState(() => _showBackTop = _scrollController.position.pixels >= 800); }); }
@override void dispose() { // 记得销毁对象 _scrollController.dispose(); super.dispose(); }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('ScrollController Demo'), ), body: ListView( controller: _scrollController, children: List.generate( 20, (index) => Container(height: 50.0, alignment: Alignment.center, child: Text('Item ${index + 1}'))), ), floatingActionButton: _showBackTop // 当需要显示的时候展示按钮,不需要的时候隐藏,设置 null ? FloatingActionButton( onPressed: () { // scrollController 通过 animateTo 方法滚动到某个具体高度 // duration 表示动画的时长,curve 表示动画的运行方式,flutter 在 Curves 提供了许多方式 _scrollController.animateTo(0.0, duration: Duration(milliseconds: 500), curve: Curves.decelerate); }, child: Icon(Icons.vertical_align_top), ) : null, ); } }

  最后的效果图

  好啦,这节就到这,下节继续填这节课留下的坑。

  代码地址:

  https://github.com/kukyxs/flutter_arts_demos_app

  今日面试问题:

  你们喜欢系列文章吗?希望连载更新吗?

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
巴基斯坦消息人士:伊朗本轮谈判立场更加强硬,强调结束战争方案必须按照伊朗的条件执行

巴基斯坦消息人士:伊朗本轮谈判立场更加强硬,强调结束战争方案必须按照伊朗的条件执行

封面新闻
2026-04-25 22:28:03
连续6个跌停板!股民:一切都结束了!

连续6个跌停板!股民:一切都结束了!

数据挖掘分析
2026-04-25 14:41:44
笑不活了!白鹿海口跑男整了两口海水,也太实在了吧!

笑不活了!白鹿海口跑男整了两口海水,也太实在了吧!

后世的君子
2026-04-25 16:27:54
袁军波突发心脏疾病,不幸离世

袁军波突发心脏疾病,不幸离世

台州交通广播
2026-04-25 18:10:05
恒大集团与特朗普合作往事

恒大集团与特朗普合作往事

地产微资讯
2026-04-24 18:09:33
“00后”陈某某被押赴刑场 执行死刑

“00后”陈某某被押赴刑场 执行死刑

闪电新闻
2026-02-07 16:53:35
梅根·马克尔透露莉莉贝特或将伴随哈里王子出席圣丹斯,延续家庭传统

梅根·马克尔透露莉莉贝特或将伴随哈里王子出席圣丹斯,延续家庭传统

奇思妙想生活家
2026-04-26 01:28:31
中美70艘驱逐舰4700导弹对决,海上较量已达关键时刻

中美70艘驱逐舰4700导弹对决,海上较量已达关键时刻

明天后天大后天
2026-04-26 01:59:09
女篮抽到下下签,宫导迎挑战!放弃3人、启用李梦、刘禹彤是良策

女篮抽到下下签,宫导迎挑战!放弃3人、启用李梦、刘禹彤是良策

米果说识
2026-04-25 08:55:28
铁证面前,还能撤案?深扒无果、信息全封,路虎车主背景有多硬?

铁证面前,还能撤案?深扒无果、信息全封,路虎车主背景有多硬?

世界圈
2026-03-24 12:52:50
涉嫌走私出口1200多吨天然鳞片石墨,濮耐股份被起诉!该原料出口受管制,“三高”石墨甚至可用于造导弹

涉嫌走私出口1200多吨天然鳞片石墨,濮耐股份被起诉!该原料出口受管制,“三高”石墨甚至可用于造导弹

每日经济新闻
2026-04-25 10:15:07
38岁老板娘沦为陪睡工具:揭秘黑茶高端骗局,入局者10有9个离婚

38岁老板娘沦为陪睡工具:揭秘黑茶高端骗局,入局者10有9个离婚

云景侃记
2026-02-12 22:21:30
64岁陈庭威:没老婆没孩子,定居广东住豪宅,不服老打球很精彩

64岁陈庭威:没老婆没孩子,定居广东住豪宅,不服老打球很精彩

阿伧说事
2026-04-14 01:36:11
我空降到家乡担任副省长,参加校友聚会,却被班花的处长丈夫嘲笑

我空降到家乡担任副省长,参加校友聚会,却被班花的处长丈夫嘲笑

红豆讲堂
2025-04-16 10:47:03
台湾专家赖岳谦:印度确实有一点比中国强很多,那就是——嘴。

台湾专家赖岳谦:印度确实有一点比中国强很多,那就是——嘴。

荆楚寰宇文枢
2026-04-22 23:16:37
尹子维的母亲曾是邵氏的顶级花旦,惊为天人的美貌,美得让人窒息

尹子维的母亲曾是邵氏的顶级花旦,惊为天人的美貌,美得让人窒息

上官晚安
2026-04-21 08:56:51
苏翊鸣朱易4年恋情结束!双方互相取关,女方晒眼泪照喊话向前走

苏翊鸣朱易4年恋情结束!双方互相取关,女方晒眼泪照喊话向前走

阿纂看事
2026-04-25 18:08:58
iPhone实现短信自动转发到微信,验证码、取件码再也不漏接

iPhone实现短信自动转发到微信,验证码、取件码再也不漏接

星哥玩云
2026-04-18 23:44:00
这才是亚洲颜值天花板,身材堪比明星,妥妥的东方美人典范

这才是亚洲颜值天花板,身材堪比明星,妥妥的东方美人典范

动物奇奇怪怪
2026-04-14 05:19:28
南京新任免4名副市长

南京新任免4名副市长

爱下厨的阿酾
2026-04-24 18:19:48
2026-04-26 02:48:49
码个蛋
码个蛋
码个蛋
808文章数 298关注度
往期回顾 全部

科技要闻

DeepSeek V4发布!黄仁勋预言的"灾难"降临

头条要闻

媒体:美军在中东罕见高密度集结 伊朗开始调整战术

头条要闻

媒体:美军在中东罕见高密度集结 伊朗开始调整战术

体育要闻

那一刻开始,两支球队的命运悄然改变了

娱乐要闻

《我们的爸爸2》第一季完美爸爸翻车了

财经要闻

90%订单消失,中东旺季没了

汽车要闻

2026款乐道L90亮相北京车展 乐道L80正式官宣

态度原创

房产
手机
亲子
数码
军事航空

房产要闻

新一轮教育大爆发来了!海口,开始疯狂建学校!

手机要闻

iPhone Ultra机模上手:11mm厚、无长焦,苹果第一折就这?

亲子要闻

父母的高期待和担心,正在给孩子制造焦虑!

数码要闻

联发科亮相2026北京车展:主动式智能体座舱解决方案

军事要闻

美防长:战事不会“没完没了”

无障碍浏览 进入关怀版