前言
在此之前,花費了很長時間學習了視圖數據流轉機制、底層渲染方案、視圖更新策略等知識,都是構成一個 UI 框架的根本,看似枯燥,卻往往具有最長久的生命力。新框架每年層出不窮,可是扒下那層炫酷的“外衣”,里面其實還是那些最基礎的知識和原理。
因此,只有把這些最基礎的知識弄明白,修煉好內功,才能觸類旁通,由點及面形成自己的知識體系,也能夠在框架之上思考應用層構建視圖實現的合理性。
在對視圖的基礎知識有了整體印象后,再來學習 Flutter 視圖系統提供所提供的 UI 控件,就會事半功倍。作為一個 UI 框架,與 Android、iOS 類似的,Flutter 自然也提供了很多 UI 控件。而文本、圖片和按鈕則是這些不同的 UI 框架中構建視圖都要用到的三個最基礎的控件。
(一)文本控件
在 Flutter 中,文本展示是通過 Text 控件實現的。
Text 支持兩種類型的文本展示,一個是默認的展示單一樣式的文本 Text,另一個是支持多種混合樣式的富文本 Text.rich。
(1)單一樣式的文本 Text
單一樣式文本 Text 的初始化,是要傳入需要展示的字符串。而這個字符串的具體展示效果,受構造函數中的其他參數控制。這些參數大致可以分為兩類:
- 控制整體文本布局的參數,如文本對齊方式 textAlign、文本排版方向 textDirection,文本顯示最大行數 maxLines、文本截斷規則 overflow 等等,這些都是構造函數中的參數;
- 控制文本展示樣式的參數,如字體名稱 fontFamily、字體大小 fontSize、文本顏色 color、文本陰影 shadows 等等,這些參數被統一封裝到了構造函數中的參數 style 中。
Text(
'Hello World',
textAlign: TextAlign.center, // 顯示居中
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red) // 20號紅色粗體
)
(2)多種混合展示樣式
混合展示樣式與單一樣式的關鍵區別在于分片,即如何把一段字符串分為幾個片段來管理,給每個片段單獨設置樣式。在 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: 'Hello', style: blackStyle),
TextSpan(text: 'World', style: redStyle)
]),
textAlign: TextAlign.center,
),
(二)圖片
使用 Image,可以讓我們向用戶展示一張圖片。圖片的顯示方式有很多,比如資源圖片、網絡圖片、文件圖片等,圖片格式也各不相同,因此在 Flutter 中也有多種方式,用來加載不同形式、支持不同格式的圖片:
- 加載本地資源圖片,如 Image.asset(‘images/logo.png’);
- 加載本地(File 文件)圖片,如 Image.file(new File(’/storage/xxx/xxx/test.jpg’));
- 加載網絡圖片,如 Image.network(<code>'http://xxx/xxx/test.gif'</code>) 。
除了可以根據圖片的顯示方式設置不同的圖片源之外,圖片的構造方法還提供了填充模式 fit、拉伸模式 centerSlice、重復模式 repeat 等屬性,可以針對圖片與目標區域的寬高比差異制定排版模式。
在加載網絡圖片的時候,為了提升用戶的等待體驗,我們往往會加入占位圖、加載動畫等元素,這時候我需要用到 FadeInImage 控件。
FadeInImage 控件提供了圖片占位的功能,并且支持在圖片加載完成時淡入淡出的視覺效果。此外,由于 Image 支持 gif 格式,我們甚至還可以將一些炫酷的加載動畫作為占位圖。
FadeInImage.assetNetwork(
placeholder: 'images/loading.gif',
image: '[https://avatars2.githubusercontent.com/u/30656290?s=400&u=09c8f2272dc7055321ef0a5a5350ddf1c6f3a293&v=4](https://avatars2.githubusercontent.com/u/30656290?s=400&u=09c8f2272dc7055321ef0a5a5350ddf1c6f3a293&v=4)
',
fit: BoxFit.cover,
width: 200,
height: 200,
)
Image 控件需要根據圖片資源異步加載的情況,決定自身的顯示效果,因此是一個 StatefulWidget。圖片加載過程由 ImageProvider 觸發,而 ImageProvider 表示異步獲取圖片數據的操作,可以從資源、文件和網絡等不同的渠道獲取圖片。
首先,ImageProvider 根據 _ImageState 中傳遞的圖片配置生成對應的圖片緩存 key;然后,去 ImageCache 中查找是否有對應的圖片緩存,如果有,則通知 _ImageState 刷新 UI;如果沒有,則啟動 ImageStream 開始異步加載,加載完畢后,更新緩存;最后,通知 _ImageState 刷新 UI。
值得注意的是,ImageCache 使用 LRU(Least Recently Ised,最近最少使用)算法進行緩存更新策略,并且默認最多儲存 1000 張圖片,最大緩存限制為 100 MB,當限定的空間已存滿數據時,把最久沒有被訪問到的圖片清除。圖片緩存只會在運行期間生效,也就是只緩存在內存中。如果想要支持緩存到文件系統,可以使用 Flutter 第三方的 CachedNetworkImage 控件。
CachedNetworkImage 的使用方法與 Image 類似,除了支持圖片緩存外,還提供了比 FadeImage 更強大的加載過程占位與加載錯誤占位,可以支持比用圖片占位更靈活的自動以控件占位。
(三)按鈕
通過按鈕,響應用戶的交互事件。Flutter 提供了三個基本的按鈕控件,FlatButton 、RaisedButton 和 FloatingActionButton。
- FlatButton:扁平化的按鈕,默認透明背景,被點擊后會呈現灰色背景。
- RaisedButton:凸起的按鈕,默認帶有灰色背景,被點擊后灰色背景會加深。
- FloatingActionButton:一個圓形的按鈕,一般出現在屏幕內容的前面,用來處理界面中最常用、最基礎的用戶動作。在 Flutter 官方示例模板中,計數器的 “+” 懸浮按鈕就是一個 FloatingActionButton。
示例代碼,縱列的布局中,分別排列三個樣式按鈕:
body: Column(
children: <Widget>[
FlatButton(
onPressed: () => print('FlatButton pressed'),
child: Text('Btn1'),
),
RaisedButton(
onPressed: () => print('RaisedButton pressed'),
child: Text('Btn2'),
),
FloatingActionButton(
onPressed: () => print('FloatingActionButton pressed'),
child: Text('Btn3'),
)
],
),
既然是按鈕,因此除了控制基本樣式之外,還需要響應用戶點擊行為。這就對應著按鈕控件中的兩個最重要的參數了:
- onPressed 參數用于設置點擊回調,告訴 Flutter 在按鈕被點擊時通知我們。如果 onPressed 參數為空,則按鈕會處于禁用狀態,不響應用戶點擊。
- child 參數用于設置按鈕的內容,告訴 Flutter 控件應該長成什么樣,也就是控制著按鈕控件的基本樣式。child 可以接收任意的 Widget,比如我們在上面的例子中傳入的 Text,除此之外我們還可以傳入 Image 等控件。
雖然我們可以通過 child 參數來控制按鈕控件的基本樣式,但是系統默認的樣式還是太單調了。因此通常情況下,我們還是會進行控件樣式定制。
與 Text 控件類似,按鈕控件也提供了豐富的樣式定制功能,比如背景顏色 color、按鈕形狀 shape、主題顏色 colorBrightness,等等。
示例代碼:
body: FlatButton(
// 設置背景色為粉色
color: Colors.pink,
// 設置斜角矩形邊框
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,保證按鈕文字顏色為深色。