文字控制元件
Text 支援兩種型別的文字展示,一個是預設的展示單一樣式文字 Text,另一個是支援多種混合樣式的富文字 Text.rich。
單一樣式文字 Text
單一樣式文字 Text 的初始化,是要傳入需要展示的字串。而這個字串的具體展示效果,受建構函式中的其他引數控制。這些引數大致可以分為兩類:
- 控制整體文字佈局的引數,如文字對齊方式 textAlign、文字排版方向 textDirection,文字顯示最大行數 maxLines、文字截斷規則 overflow 等等,這些都是建構函式中的引數;
- 控制文字展示樣式的引數,如字型名稱 fontFamily、字型大小 fontSize、文字顏色 color、文字陰影 shadows 等等,這些引數被統一封裝到建構函式中的引數 style 中。
示例程式碼 - 定義了一段劇中佈局、20號紅色粗體展示樣式的字串:
Text(
'文字是檢視系統中的常見空間,用來顯示一段特定樣式的字串,就比如 Android 裡的 TextView,或是 iOS 中的 UILabel。',
textAlign: TextAlign.center, // 居中顯示
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red),// 20 號,紅色粗體展示
);
複製程式碼
支援多種混合樣式的富文字 Text.rich
混合展示樣式與單一樣式的關鍵區別在於分片,即如何把一段字串分為幾個片段來管理,給每個片段單獨設定樣式。在 Flutter 中可以使用 TextSpan。
TextSpan 定義來一個字串片段該如何控制其展示樣式,而將這些有著獨立展示樣式的字串組裝在一起,則可以支援混合樣式的富文字展示。
示例程式碼 - 分別定義黑色與紅色兩種展示樣式
TextStyle blackStyle = TextStyle(fontWeight: FontWeight.normal, fontSize: 20, color: Colors.black); // 黑色樣式
TextStyle redStyle = TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red); // 紅色樣式
Text.rich(
TextSpan(
children: <TextSpan>[
TextSpan(text: '文字是檢視系統中的常見空間,用來顯示一段特定樣式的字串,就比如', style: redStyle), // 第 1 個片段,紅色樣式
TextSpan(text: 'Android', style: blackStyle), // 第 1 個片段,黑色樣式
TextSpan(text: '中的', style: redStyle), // 第 1 個片段,紅色樣式
TextSpan(text: 'TextView', style: blackStyle), // 第 1 個片段,黑色樣式
]
),
textAlign: TextAlign.center,
);
複製程式碼
圖片
在 Flutter 中有多張方式,用來載入不同形式、支援不同格式的圖片:
- 載入本地資源圖片,如 Image.asset('images/log.png');
- 載入本地(File 檔案)圖片,如 Image.file(new File('/storage/xxx/xxx/test.jpg'));
- 載入網路圖片,如 Image.network('http://xxx/xxx/test.gif')。
除了可以根據圖片的顯示方式設定不同的圖片源之外,圖片的構造方法還提供了填充模式 fit、拉伸模式 centerSlice、重複模式 repeat 等屬性,可以針對圖片與目標區域的寬高比差異制定排版模式。
FadeInImage 控制元件
在載入網路圖片的時候,為了提升使用者的等待體驗,往往會加入佔點陣圖、載入動畫等元素,但是預設的 Image.network 構造方法並不支援這些高階功能,這時候 FadeInImage 控制元件就派上用場了。
FadeInImage 控制元件提供了圖片佔位的功能,並且支援在圖片載入完成時淡入淡出的視覺效果。此外,由於 Image 支援 gif 格式,甚至還可以將一些炫酷的載入動畫作為佔點陣圖。
示例程式碼 - loading 的 gif 作為佔點陣圖展示:
FadeInImage.assetNetwork(
placeholder: 'asssets/loading.gif', // gif 佔位
image: 'https://xxx/xxx/xxx.jpg',
fit: BoxFit.cover, // 圖片拉伸模式
width: 200,
height: 200,
);
複製程式碼
Image 控制元件需要根據圖片資源非同步載入的情況,決定自身的顯示效果,因此是一個 StatefulWidget。圖片載入過程由 ImageProvider 觸發,而 ImageProvider 表示非同步獲取圖片資料的操作,可以從資源、檔案和網路等不同的渠道獲取圖片。
首先,ImageProvider 根據 _imageSate 中傳遞的圖片配置生成對應的圖片快取 key;然後,去 ImageCache 中查詢是否有對應的圖片快取,如果有,則通知 _ImageState 重新整理 UI;如果沒有,則啟動 ImageStream 開始非同步載入,載入完畢後,更新快取;最後通知 _imageSate 重新整理 UI。
值得注意的是,ImageCache 使用 LRU(Least Recently Used,最近最少使用)演算法進行快取更新策略,並且預設最多儲存 1000 張圖片,最大快取限制為 100 MB,當限定的空間已經存滿資料時,把最久沒有被訪問到的圖片清除。圖片 快取只會在執行期間生效,也就是隻快取在記憶體中。如果想要支援快取到檔案系統,可以使用第三方的 CachedNetworkImage
控制元件。
按鈕
通過按鈕,可以相應使用者的互動事件。Flutter 提供了三個基本的按鈕空間,即 FloatingActionButton、FlatButton 和 RaisedButton。
- FloatingActionButton:一個圓形的按鈕,一般出現在螢幕內容的前面,用來處理介面中最常用、最基礎的使用者動作。
- RaisedButton:凸起的按鈕,預設帶有灰色背景,被點選後灰色背景會加深。
- FlatButton:扁平化的按鈕,預設透明背景,被點選後呈現灰色背景。
既然是按鈕,因此除了控制基本樣式之外,還需要響應使用者點選行為。這就對應著按鈕空間中的兩個最重要的引數:
- onPressed 引數用於設定點選回撥,告訴 Flutter 在按鈕被點選時通知我們。如果 onPressed 引數為空,則按鈕會處於禁用狀態,不響應使用者點選。
- child 引數用於設定按鈕的內容,告訴 Flutter 控制元件應該長成什麼樣,也就是控制著按鈕控制元件的基本樣式。child 可以接收任意的 Widget。
除此之外,還可以進行樣式定製(以 FlatButton 為例):
FlatButton(
color: Colors.yellow, // 設定背景色為黃色
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(20.0)), // 設定斜角矩形邊框
colorBrightness: Brightness.light, // 確保文字按鈕為深色
onPressed: () => print('FlatButton pressed'),
child: Row(children: <Widget>[Icon(Icons.add), Text("Add")],),
);
複製程式碼
ListView
若相關基本元素的排列布局超過螢幕顯示尺寸(即超過一屏)時,就需要引入列表控制元件來展示檢視的完整內容,並根據元素的多少進行自適應滾動展示。
這樣的需求,在 Android 中是由 ListView 或 RecyclerView 實現的,在 iOS 中是用 UITableView 實現的;而在 Flutter 中,實現這種需求的則是列表空間 ListView。
在 Flutter 中,ListView 可以沿著一個方向(垂直或水平方向)來排列其所有子 Widget,因此常用於需要展示一組連續檢視元素的場景,比如通訊錄、優惠劵、商家列表等。
ListView 提供了一個預設建構函式 ListView,可以通過設定它的 children 引數,很方便地將所有的子 Widget 包含到 ListView 中。
但是,這種建立方式要求提前將所有的子 Widget 一次性建立好,而不是等到它們真正在螢幕上需要顯示時才建立,所以有一個很明顯的缺點,就是效能不好。因此,這種方式僅適用於列表中含有少量元素的場景。
ListView(
scrollDirection: Axis.horizontal, // 設定滾動方向
children: <Widget>[
// 設定 ListTile 元件的標題與圖示
ListTile(leading: Icon(Icons.map), title: Text('Map'),),
ListTile(leading: Icon(Icons.mail), title: Text('Mail'),),
ListTile(leading: Icon(Icons.message), title: Text('Message'),),
],
);
複製程式碼
**ListView 的另外一個建構函式 ListView.builder,則適用於子 Widget 比較多的場景。這個建構函式有兩個關鍵引數:
- itemBuilder,是列表項的建立方法。當列表滾動到相應位置時,ListView 會呼叫該方法建立對應的子 Widget。
- itemCount,表示列表項的數量,如果為空,則表示 ListView 為無限列表。
具體用法如下,定義一個擁有 100 個列表元素的 ListView:
ListView.builder(
itemCount: 100, // 元素個數
itemExtent: 50.0, // 列表項高度
itemBuilder: (BuildContext context, int index) => ListTile(title: Text("title $index"), subtitle: Text("body $index"),),
);
複製程式碼
需要注意的是,itemExtent並不是一個必填引數。但,對於定高的列表項元素,強烈建議提前設定好這個引數的值。
因為如果這個引數為 null,ListView 會動態地根據子 Widget 建立完成的結果,決定自身的檢視高度,以及子 Widget 在 ListView 中的相對位置。在滾動發生變化而列表項又很多時,這樣的計算就會非常頻繁。
在 ListView 中,有兩種方式支援分割線:
- 一種是,在 itemBuilder 中,根據 index 的值動態建立分割線,也就是將分割線視為列表項的一部分;
- 另一種是,使用 ListView 的另一個構造方法 ListView.separated,單獨設定分割線的樣式。
總結 ListView 常見的構造方法及適用場景如下:
建構函式名 | 特點 | 適用場景 | 使用頻次 |
---|---|---|---|
ListView | 一次性建立好全部子 Widget | 適用於展示少量連續子 Widget 的場景 | 中 |
List.builder | 提供了子 Widget 建立方法,僅在需要展示時才建立 | 適用於子 Widget 較多,且視覺效果呈現某種規律性的場景 | 高 |
ListView.separated | 與 ListView.builder 類似,並提供了自定義分割線的功能 | 與 ListView.builder 場景類似 | 中 |
CustomScrollView
ListView 實現了單一檢視下可滾動 Widget 的互動模式,同時也包含了 UI 顯示相關的控制邏輯和佈局模型。但是,對於某些特殊互動場景,比如多個效果聯動、巢狀滾動、精細滑動、檢視跟隨手勢操作等,還需要巢狀多個 ListView 來實現。這時,各自檢視的滾動和佈局模型就是相互獨立、分離的,就很難保證整個頁面統一一致的滑動效果。
在 Flutter 中有一個專門的控制元件 CustomScrollView,用來處理多個需要自定義滾動效果的 Widget。在 CustomScrollView 中,這些彼此獨立的、可滾動的 Widget 被統稱為 Sliver。
比如,ListView 的 Sliver 實現為 SliverList,AppBar 的 Sliver 實現為 SliverAppBar。這些 Sliver 不再維護各自的滾動狀態,而是交由 CustomScrollView 統一管理,最終實現滑動效果的一致性。
可以通過一個滾動視差的例子,演示 CustomScrollView 的使用方法。
視差滾動是指讓多層背景以不同的速度移動,在形成立體滾動效果的同時,還能保證良好的視覺體驗。作為移動應用互動設計的熱點趨勢,越來越多的移動應用使用來這項技術。
以一個有著封面頭圖的列表為例,封面頭圖和列表這兩層檢視的滾動聯動起來,當使用者滾動列表時,頭圖會根據使用者的滾動手勢,進行縮小和展開。
經過分析得出,要實現這樣的需求,需要兩個 Sliver:作為頭圖的 SliverAppBar,作為列表的 SliverList。思路如下:
- 在建立 SliverAppBar 時,把 flexibleSpace 引數設定為懸浮頭圖背景。flexibleSpace 可以讓背景圖顯示在 AppBar 下方,高度和 SliverAppBar 一樣;
- 而在建立 SliverList 時,通過 SliverChildBuilderDelegate 引數實現列表項元素的建立;
- 最後,將它們一併交由 CustomScrollView 的 slivers 引數統一管理。
具體的示例程式碼如下:
CustomScrollView (
slivers: <Widget>[
SliverAppBar( // SliverAppBar 作為頭圖控制元件
title: Text('CustomScrollView Demo'), // 標題
floating: true, // 設定懸浮樣式
flexibleSpace: Image.network("https://xx.jpg", fit: BoxFit.cover,), // 設定懸浮頭圖背景
expandedHeight: 300, // 頭圖控制元件高度
),
SliverList( // SliverList 作為列表控制元件
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('Item #$index'),), //列表項建立方法
childCount: 100, // 列表元素個數
),
)
],
);
複製程式碼
ScrollController 與 ScrollNotification
使用 ScrollController 進行滾動資訊的監聽,以及相應的滾動控制;ScrollNotifiCation 通知進行滾動事件的獲取。
在 Flutter 中,因為 Widget 並不是渲染到螢幕的最終視覺元素(RenderObject 才是),所以無法像原生的 Android 或 iOS 系統那樣,向持有的 Widget 物件獲取或者設定最終渲染相關的視覺資訊,而必須通過對應的元件控制器才能實現。
ListView 的元件控制器則是 ScrollController,我們可以通過它來獲取檢視的滾動資訊,更新檢視的滾動位置。
一般而言,獲取檢視的滾動資訊往往是為了進行介面的狀態控制,因此 ScrollController 的初始化、監聽及銷燬需要與 StatefulWidget 的狀態保持同步。
程式碼示例所示,宣告一個有著 100 個元素的列表項,當滾動檢視到特定位置後,使用者可以點選按鈕返回列表頂部:
- 首先,在 State 的初始化方法裡,建立了 ScrollController,並通過 _controller.addListener 註冊了滾動監聽方法回撥,根據當前檢視的滾動位置,判斷當前是否需要展示“Top”按鈕。
- 隨後,在檢視構建方法 build 中,將 ScrollController 物件與 ListView 進行了關聯,並且在 RaisedButton 中註冊了對應的回撥方法,可以在點選按鈕時通過 _controller.animateTo 方法返回列表頂部。
- 最後,在 State 的銷燬方法中,對 ScrollController 進行了資源釋放。
class _MyAppState extends State<MyApp> {
ScrollController _controller; // ListView 控制器
bool isToTop = false; // 標示目前是否需要啟用 "Top" 按鈕
@override
void initState() {
_controller = ScrollController();
_controller.addListener(() { // 為控制器註冊滾動監聽方法
if (_controller.offset > 1000) { // 如果 ListView 已經向下滾動了 1000,則啟用 Top 按鈕
setState(() {
isToTop = true;
});
}
else if (_controller.offset < 300) { // 如果 ListView 向下滾動距離不足 300,則禁用 Top 按鈕
setState(() {
isToTop = false;
});
super.initState();
}
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Scroll Controller Widget"),),
body: Column(
children: <Widget>[
Container(
height: 40.0,
child: RaisedButton(onPressed: (isToTop ? () {
if (isToTop) {
_controller.animateTo(.0, duration: Duration(milliseconds: 200), curve: Curves.ease);
}
} : null), child: Text("Top"),),
),
Expanded(
child: ListView.builder(
controller: _controller, // 初始化傳入控制器
itemCount: 100, // 列表元素總和
itemBuilder: (context, index) => ListTile(title: Text("Index : $index"),) // 列表項構造方法
),
)
],
),
);
}
@override
void dispose() {
_controller.dispose(); // 銷燬控制器
super.dispose();
}
}
複製程式碼
在 Flutter 中, ScroNotification 通知的獲取是通過 NotificationListener 來實現的。與 ScrollController 不同的是,NotificationListener 是一個 Widget,為了監聽滾動型別的事件,我們需要將 NotificationListener 新增為 ListView 的父容器,從而捕獲 ListView 中的通知。而這些通知,需要通過 onNotification 回撥函式實現監聽邏輯:
Widget build(BuildContext context) {
return MaterialApp(
title: 'ScrollController Demo',
home: Scaffold(
appBar: AppBar(title: Text('ScrollController Demo')),
body: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollStartNotification) { // 開始滾動
}
else if ( scrollNotification is ScrollUpdateNotification) { // 滾動位置更新
}
else if (ScrollStartNotification is ScrollEndNotification) { // 滾動結束
}
},
child: ListView.builder(itemBuilder: (context, index) => ListTile(title: Text("Index : $index"),));
),
),
);
}
複製程式碼
相比於 ScrollController 只能和具體的 ListView 關聯後才可以監聽到滾動資訊;通過 NotificationListener 則可以監聽其子 Widget 中的任意 ListView,不僅可以得到這些 ListView 的當前滾動位置資訊,還可以獲取當前的滾動事件資訊。