該文已授權公眾號 「碼個蛋」,轉載請指明出處
前面的小節基本上講完了常用的部件和容器部件,也可以完成很多的界面,但是又一個問題,假如我們要顯示一段文字,比如將 一段又臭又長的文字
在界面上顯示 1000 次,不難完成吧
// ..省略一些無關代碼
body: Text('一段又臭又長的文字' * 1000, softWrap: true)
很簡單,運行到手機...「誒誒誒,**,怎么只顯示了一部分,剩下的怎么畫不下去」
日常開發中,會遇到很多這種情況,許多界面不是一頁就能夠顯示完的。那么這里提下可滑動的容器部件
SingleChildScrollView
這個部件非常簡單,不貼源碼了。最簡單的使用方式只需要提供一個 child
即可。現在給前面寫的 Text
包裹上一層 SingleChildScrollView
然后再運行,文字全部都展示出來了。
如果需要實現一個垂直的滾動列表,可以直接通過 SingleChildScrollView
包裹 Column
來實現,列表內容全部塞到 Column
即可
class SingleChildScrollDemoPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
/// letters 自由發揮吧...一定要大量,大量,大量
List<String> 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<String, List<String>> _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<String> letters = [ ..... ];
// 用于區分網格單元
final List<Color> 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<String> letters = [ ..... ];
final List<Color> 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: <Widget>[
// 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: <Widget>[
Divider(height: 2.0, color: Colors.black54),
Stack(
alignment: Alignment.center,
children: <Widget>[
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<ScrollControllerDemoPage> {
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,
);
}
}
最后的效果圖
好啦,這節就到這,下節繼續填這節課留下的坑。
最后代碼的地址還是要的:
文章中涉及的代碼:demos
基于郭神
cool weather
接口的一個項目,實現BLoC
模式,實現狀態管理:flutter_weather一個課程(當時買了想看下代碼規范的,代碼更新會比較慢,雖然是跟著課上的一些寫代碼,但是還是做了自己的修改,很多地方看著不舒服,然后就改成自己的實現方式了):flutter_shop
如果對你有幫助的話,記得給個 Star,先謝過,你的認可就是支持我繼續寫下去的動力~