在上一篇文章中,我與你介紹了 Widget 生命周期的實際承載者 State,并詳細介紹了初始化、狀態更新與控件銷毀,這 3 個不同階段所涉及的關鍵方法調用順序。深入理解視圖從加載到構建再到銷毀的過程,可以幫助你理解如何根據視圖的狀態在合適的時機做恰當的事情。
前面幾次分享我們講了很多關于 Flutter 框架視圖渲染的基礎知識和原理。但有些同學可能會覺得這些基礎知識和原理在實踐中并不常用,所以在學習時會選擇忽視這些內容。
但其實,像視圖數據流轉機制、底層渲染方案、視圖更新策略等知識,都是構成一個 UI 框架的根本,看似枯燥,卻往往具有最長久的生命力。新框架每年層出不窮,可是扒下那層炫酷的“外衣”,里面其實還是那些最基礎的知識和原理。
因此,**只有把這些最基礎的知識弄明白了,修煉好了內功,才能觸類旁通,由點及面形成自己的知識體系,也能夠在框架之上思考應用層構建視圖實現的合理性。**
在對視圖的基礎知識有了整體印象后,我們再來學習 Flutter 視圖系統所提供的 UI 控件,就會事半功倍了。而作為一個 UI 框架,與 Android、iOS 和 React 類似的,Flutter 自然也提供了很多 UI 控件。而文本、圖片和按鈕則是這些不同的 UI 框架中構建視圖都要用到的三個最基本的控件。因此,在今天這篇文章中,我就與你一起學習在 Flutter 中該如何使用它們。
## 文本控件
文本是視圖系統中的常見控件,用來顯示一段特定樣式的字符串,就比如 Android 里的 TextView、iOS 中的 UILabel。而在 Flutter 中,文本展示是通過 Text 控件實現的。
Text 支持兩種類型的文本展示,一個是默認的展示單一樣式的文本 Text,另一個是支持多種混合樣式的富文本 Text.rich。
我們先來看看**如何使用單一樣式的文本 Text**。
單一樣式文本 Text 的初始化,是要傳入需要展示的字符串。而這個字符串的具體展示效果,受構造函數中的其他參數控制。這些參數大致可以分為兩類:
* **控制整體文本布局的參數**,如文本對齊方式 textAlign、文本排版方向 textDirection,文本顯示最大行數 maxLines、文本截斷規則 overflow 等等,這些都是構造函數中的參數;
* **控制文本展示樣式的參數**,如字體名稱 fontFamily、字體大小 fontSize、文本顏色 color、文本陰影 shadows 等等,這些參數被統一封裝到了構造函數中的參數 style 中。
接下來,我們以一個具體的例子來看看 Text 控件的使用方法。如下所示,我在代碼中定義了一段居中布局、20 號紅色粗體展示樣式的字符串:
~~~
Text(
'文本是視圖系統中的常見控件,用來顯示一段特定樣式的字符串,就比如 Android 里的 TextView,或是 iOS 中的 UILabel。',
textAlign: TextAlign.center,// 居中顯示
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red),//20 號紅色粗體展示
);
~~~
運行效果如下圖所示:
:-: 
圖 1 單一樣式文本 Text 示例
理解了展示單一樣式的文本 Text 的使用方法后,我們再來看看**如何在一段字符串中支持多種混合展示樣式**。
**混合展示樣式與單一樣式的關鍵區別在于分片**,即如何把一段字符串分為幾個片段來管理,給每個片段單獨設置樣式。面對這樣的需求,在 Android 中,我們使用 SpannableString 來實現;在 iOS 中,我們使用 NSAttributedString 來實現;而在 Flutter 中也有類似的概念,即 TextSpan。
TextSpan 定義了一個字符串片段該如何控制其展示樣式,而將這些有著獨立展示樣式的字符串組裝在一起,則可以支持混合樣式的富文本展示。
如下方代碼所示,我們分別定義了黑色與紅色兩種展示樣式,隨后把一段字符串分成了 4 個片段,并設置了不同的展示樣式:
~~~
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,
);
~~~
運行效果,如下圖所示:
:-:
圖 2 混合樣式富文本 Text.rich 示例
接下來,我們再看看 Flutter 中的圖片控件 Image。
## 圖片
使用 Image,可以讓我們向用戶展示一張圖片。圖片的顯示方式有很多,比如資源圖片、網絡圖片、文件圖片等,圖片格式也各不相同,因此在 Flutter 中也有多種方式,用來加載不同形式、支持不同格式的圖片:
* 加載本地資源圖片,如 Image.asset(‘images/logo.png’);
* 加載本地(File 文件)圖片,如 Image.file(new File(’/storage/xxx/xxx/test.jpg’));
* 加載網絡圖片,如 Image.network(`'http://xxx/xxx/test.gif'`) 。
除了可以根據圖片的顯示方式設置不同的圖片源之外,圖片的構造方法還提供了填充模式 fit、拉伸模式 centerSlice、重復模式 repeat 等屬性,可以針對圖片與目標區域的寬高比差異制定排版模式。
這,和 Android 中 ImageView、iOS 里的 UIImageView 的屬性都是類似的。因此,我在這里就不再過多展開了。你可以參考官方文檔中的[Image 的構造函數](https://api.flutter.dev/flutter/widgets/Image/Image.html)部分,去查看 Image 控件的具體使用方法。
關于圖片展示,我還要和你分享下 Flutter 中的**FadeInImage**控件。在加載網絡圖片的時候,為了提升用戶的等待體驗,我們往往會加入占位圖、加載動畫等元素,但是默認的 Image.network 構造方法并不支持這些高級功能,這時候 FadeInImage 控件就派上用場了。
FadeInImage 控件提供了圖片占位的功能,并且支持在圖片加載完成時淡入淡出的視覺效果。此外,由于 Image 支持 gif 格式,我們甚至還可以將一些炫酷的加載動畫作為占位圖。
下述代碼展示了這樣的場景。我們在加載大圖片時,將一張 loading 的 gif 作為占位圖展示給用戶:
~~~
FadeInImage.assetNetwork(
placeholder: 'assets/loading.gif', //gif 占位
image: 'https://xxx/xxx/xxx.jpg',
fit: BoxFit.cover, // 圖片拉伸模式
width: 200,
height: 200,
)
~~~
:-: 
圖 3 FadeInImage 占位圖
Image 控件需要根據圖片資源異步加載的情況,決定自身的顯示效果,因此是一個 StatefulWidget。圖片加載過程由 ImageProvider 觸發,而 ImageProvider 表示異步獲取圖片數據的操作,可以從資源、文件和網絡等不同的渠道獲取圖片。
首先,ImageProvider 根據 \_ImageState 中傳遞的圖片配置生成對應的圖片緩存 key;然后,去 ImageCache 中查找是否有對應的圖片緩存,如果有,則通知 \_ImageState 刷新 UI;如果沒有,則啟動 ImageStream 開始異步加載,加載完畢后,更新緩存;最后,通知 \_ImageState 刷新 UI。
圖片展示的流程,可以用以下流程圖表示:
:-: 
圖 4 圖片加載流程
值得注意的是,ImageCache 使用 LRU(Least Recently Used,最近最少使用)算法進行緩存更新策略,并且默認最多存儲 1000 張圖片,最大緩存限制為 100MB,當限定的空間已存滿數據時,把最久沒有被訪問到的圖片清除。圖片**緩存只會在運行期間生效,也就是只緩存在內存中**。如果想要支持緩存到文件系統,可以使用第三方的[CachedNetworkImage](https://pub.dev/packages/cached_network_image/)控件。
CachedNetworkImage 的使用方法與 Image 類似,除了支持圖片緩存外,還提供了比 FadeInImage 更為強大的加載過程占位與加載錯誤占位,可以支持比用圖片占位更靈活的自定義控件占位。
在下面的代碼中,我們在加載圖片時,不僅給用戶展示了作為占位的轉圈 loading,還提供了一個錯誤圖兜底,以備圖片加載出錯:
~~~
CachedNetworkImage(
imageUrl: "http://xxx/xxx/jpg",
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
)
~~~
最后,我們再來看看 Flutter 中的按鈕控件。
## 按鈕
通過按鈕,我們可以響應用戶的交互事件。Flutter 提供了三個基本的按鈕控件,即 FloatingActionButton、FlatButton 和 RaisedButton。
* FloatingActionButton:一個圓形的按鈕,一般出現在屏幕內容的前面,用來處理界面中最常用、最基礎的用戶動作。在之前的第 5 篇文章“[從標準模板入手,體會 Flutter 代碼是如何運行在原生系統上的](https://time.geekbang.org/column/article/106199)”中,計數器示例的“+”懸浮按鈕就是一個 FloatingActionButton。
* RaisedButton:凸起的按鈕,默認帶有灰色背景,被點擊后灰色背景會加深。
* FlatButton:扁平化的按鈕,默認透明背景,被點擊后會呈現灰色背景。
這三個按鈕控件的使用方法類似,唯一的區別只是默認樣式不同而已。
下述代碼中,我分別定義了 FloatingActionButton、FlatButton 與 RaisedButton,它們的功能完全一樣,在點擊時打印一段文字:
~~~
FloatingActionButton(onPressed: () => print('FloatingActionButton pressed'),child: Text('Btn'),);
FlatButton(onPressed: () => print('FlatButton pressed'),child: Text('Btn'),);
RaisedButton(onPressed: () => print('RaisedButton pressed'),child: Text('Btn'),);
~~~
:-: 
圖 5 按鈕控件
既然是按鈕,因此除了控制基本樣式之外,還需要響應用戶點擊行為。這就對應著按鈕控件中的兩個最重要的參數了:
* onPressed 參數用于設置點擊回調,告訴 Flutter 在按鈕被點擊時通知我們。如果 onPressed 參數為空,則按鈕會處于禁用狀態,不響應用戶點擊。
* child 參數用于設置按鈕的內容,告訴 Flutter 控件應該長成什么樣,也就是控制著按鈕控件的基本樣式。child 可以接收任意的 Widget,比如我們在上面的例子中傳入的 Text,除此之外我們還可以傳入 Image 等控件。
雖然我們可以通過 child 參數來控制按鈕控件的基本樣式,但是系統默認的樣式還是太單調了。因此通常情況下,我們還是會進行控件樣式定制。
與 Text 控件類似,按鈕控件也提供了豐富的樣式定制功能,比如背景顏色 color、按鈕形狀 shape、主題顏色 colorBrightness,等等。
接下來,我就以 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")],)
);
~~~
可以看到,我們將一個加號 Icon 與文本組合,定義了按鈕的基本外觀;隨后通過 shape 來指定其外形為一個斜角矩形邊框,并將按鈕的背景色設置為黃色。
因為按鈕背景顏色是淺色的,為避免按鈕文字看不清楚,我們通過設置按鈕主題 colorBrightness 為 Brightness.light,保證按鈕文字顏色為深色。
展示效果如下:
:-: 
圖 6 按鈕控件定制外觀
## 總結
UI 控件是構建一個視圖的基本元素,而文本、圖片和按鈕則是其中最經典的控件。
接下來,我們簡單回顧一下今天的內容,以便加深理解與記憶。
首先,我們認識了支持單一樣式和混合樣式兩種類型的文本展示控件 Text。其中,通過 TextStyle 控制字符串的展示樣式,其他參數控制文本布局,可以實現單一樣式的文本展示;而通過 TextSpan 將字符串分割為若干片段,對每個片段單獨設置樣式后組裝,可以實現支持混合樣式的富文本展示。
然后,我帶你學習了支持多種圖片源加載方式的圖片控件 Image。Image 內部通過 ImageProvider 根據緩存狀態,觸發異步加載流程,通知 \_ImageState 刷新 UI。不過,由于圖片緩存是內存緩存,因此只在運行期間生效。如果要支持緩存到文件系統,可以使用第三方的 CachedNetworkImage。
最后,我們學習了按鈕控件。Flutter 提供了多種按鈕控件,而它們的使用方法也都類似。其中,控件初始化的 child 參數用于設置按鈕長什么樣,而 onPressed 參數則用于設置點擊回調。與 Text 類似,按鈕內部也有豐富的 UI 定制接口,可以滿足開發者的需求。
通過今天的學習,我們可以發現,在 UI 基本信息的表達上,Flutter 的經典控件與原生 Android、iOS 系統提供的控件沒有什么本質區別。但是,在自定義控件樣式上,Flutter 的這些經典控件提供了強大而簡潔的擴展能力,使得我們可以快速開發出功能復雜、樣式豐富的頁面。
## 思考題
最后,我給你留下一道思考題吧。
請你打開 IDE,閱讀 Flutter SDK 中 Text、Image、FadeInImage,以及按鈕控件 FloatingActionButton、FlatButton 與 RaisedButton 的源碼,在 build 函數中找出在內部真正承載其視覺功能的控件。請和我分享下,你在這一過程中發現了什么現象?
- 前言
- 開篇詞
- 預習篇
- 01丨預習篇 · 從0開始搭建Flutter工程環境
- 02丨預習篇 · Dart語言概覽
- Flutter開發起步
- 03丨深入理解跨平臺方案的歷史發展邏輯
- 04丨Flutter區別于其他方案的關鍵技術是什么?
- 05丨從標準模板入手,體會Flutter代碼是如何運行在原生系統上的
- Dart語言基礎
- 06丨基礎語法與類型變量:Dart是如何表示信息的?
- 07丨函數、類與運算符:Dart是如何處理信息的?
- 08丨綜合案例:掌握Dart核心特性
- Flutter基礎
- 09丨Widget,構建Flutter界面的基石
- 10丨Widget中的State到底是什么?
- 11丨提到生命周期,我們是在說什么?
- 12丨經典控件(一):文本、圖片和按鈕在Flutter中怎么用?
- 13丨ListView在Flutter中是什么?
- 14 丨 經典布局:如何定義子控件在父容器中排版位置?
- 15 丨 組合與自繪,我該選用何種方式自定義Widget?
- 16 丨 從夜間模式說起,如何定制不同風格的App主題?
- 17丨依賴管理(一):圖片、配置和字體在Flutter中怎么用?
- 18丨依賴管理(二):第三方組件庫在Flutter中要如何管理?
- 19丨用戶交互事件該如何響應?
- 20丨關于跨組件傳遞數據,你只需要記住這三招
- 21丨路由與導航,Flutter是這樣實現頁面切換的
- Flutter進階
- 22丨如何構造炫酷的動畫效果?
- 23丨單線程模型怎么保證UI運行流暢?
- 24丨HTTP網絡編程與JSON解析
- 25丨本地存儲與數據庫的使用和優化
- 26丨如何在Dart層兼容Android-iOS平臺特定實現?(一)
- 27丨如何在Dart層兼容Android-iOS平臺特定實現?(二)
- 28丨如何在原生應用中混編Flutter工程?
- 29丨混合開發,該用何種方案管理導航棧?
- 30丨為什么需要做狀態管理,怎么做?
- 31丨如何實現原生推送能力?
- 32丨適配國際化,除了多語言我們還需要注意什么
- 33丨如何適配不同分辨率的手機屏幕?
- 34丨如何理解Flutter的編譯模式?
- 35丨HotReload是怎么做到的?
- 36丨如何通過工具鏈優化開發調試效率?
- 37丨如何檢測并優化FlutterApp的整體性能表現?
- 38丨如何通過自動化測試提高交付質量?
- Flutter綜合應用
- 39丨線上出現問題,該如何做好異常捕獲與信息采集?
- 40丨衡量FlutterApp線上質量,我們需要關注這三個指標
- 41丨組件化和平臺化,該如何組織合理穩定的Flutter工程結構?
- 42丨如何構建高效的FlutterApp打包發布環境?
- 43丨如何構建自己的Flutter混合開發框架(一)?
- 44丨如何構建自己的Flutter混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略