在上一篇文章中,我與你分享了在 Flutter 中實現國際化的基本原理。與原生 Android 和 iOS 只需為國際化資源提供不同的目錄,就可以在運行時自動根據語言和地區進行適配不同,Flutter 的國際化是完全在代碼中實現的。
即通過代碼聲明的方式,將應用中所有需要翻譯的文案都聲明為 LocalizationsDelegate 的屬性,然后針對不同的語言和地區進行手動翻譯適配,最后在初始化應用程序時,將這個代理設置為國際化的翻譯回調。而為了簡化這個過程,也為了將國際化資源與代碼實現分離,我們通常會使用 arb 文件存儲不同語言地區的映射關系,并通過 Flutter i18n 插件來實現代碼的自動生成。
可以說,國際化為全世界的用戶提供了統一而標準的體驗。那么,為不同尺寸、不同旋轉方向的手機提供統一而標準的體驗,就是屏幕適配需要解決的問題了。
在移動應用的世界中,頁面是由控件組成的。如果我們支持的設備只有普通手機,可以確保同一個頁面、同一個控件,在不同的手機屏幕上的顯示效果是基本一致的。但,隨著平板電腦和類平板電腦等超大屏手機越來越普及,很多原本只在普通手機上運行的應用也逐漸跑在了平板上。
但,由于平板電腦的屏幕非常大,展示適配普通手機的界面和控件時,可能會出現 UI 異常的情況。比如,對于新聞類手機應用來說,通常會有新聞列表和新聞詳情兩個頁面,如果我們把這兩個頁面原封不動地搬到平板電腦上,就會出現控件被拉伸、文字過小過密、圖片清晰度不夠、屏幕空間被浪費的異常體驗。
而另一方面,即使對于同一臺手機或平板電腦來說,屏幕的寬高配置也不是一成不變的。因為加速度傳感器的存在,所以當我們旋轉屏幕時,屏幕寬高配置會發生逆轉,即垂直方向與水平方向的布局行為會互相交換,從而導致控件被拉伸等 UI 異常問題。
因此,為了讓用戶在不同的屏幕寬高配置下獲得最佳的體驗,我們不僅需要對平板進行屏幕適配,充分利用額外可用的屏幕空間,也需要在屏幕方向改變時重新排列控件。即,我們需要優化應用程序的界面布局,為用戶提供新功能、展示新內容,以將拉伸變形的界面和控件替換為更自然的布局,將單一的視圖合并為復合視圖。
在原生 Android 或 iOS 中,這種在同一頁面實現不同布局的行為,我們通常會準備多個布局文件,通過判斷當前屏幕分辨率來決定應該使用哪套布局方式。在 Flutter 中,屏幕適配的原理也非常類似,只不過 Flutter 并沒有布局文件的概念,我們需要準備多個布局來實現。
那么今天,我們就來分別來看一下如何通過多個布局,實現適配屏幕旋轉與平板電腦。
## 適配屏幕旋轉
在屏幕方向改變時,屏幕寬高配置也會發生逆轉:從豎屏模式變成橫屏模式,原來的寬變成了高(垂直方向上的布局空間更短了),而高則變成了寬(水平方向上的布局空間更長了)。
通常情況下,由于 ScrollView 和 ListView 的存在,我們基本上不需要擔心垂直方向上布局空間更短的問題,大不了一屏少顯示幾個控件元素,用戶仍然可以使用與豎屏模式同樣的交互滾動視圖來查看其他控件元素;但水平方向上布局空間更長,界面和控件通常已被嚴重拉伸,原有的布局方式和交互方式都需要做較大調整。
從橫屏模式切回豎屏模式,也是這個道理。
為了適配豎屏模式與橫屏模式,我們需要準備兩個布局方案,一個用于縱向,一個用于橫向。當設備改變方向時,Flutter 會通知我們重建布局:Flutter 提供的 OrientationBuilder 控件,可以在設備改變方向時,通過 builder 函數回調告知其狀態。這樣,我們就可以根據回調函數提供的 orientation 參數,來識別當前設備究竟是處于橫屏(landscape)還是豎屏(portrait)狀態,從而刷新界面。
如下所示的代碼演示了 OrientationBuilder 的具體用法。我們在其 builder 回調函數中,準確地識別出了設備方向,并對橫屏和豎屏兩種模型加載了不同的布局方式,而 \_buildVerticalLayout 和 \_buildHorizo??ntalLayout 是用于創建相應布局的方法:
~~~
@overrideWidget build(BuildContext context) { return Scaffold( // 使用 OrientationBuilder 的 builder 模式感知屏幕旋轉 body: OrientationBuilder( builder: (context, orientation) { // 根據屏幕旋轉方向返回不同布局行為 return orientation == Orientation.portrait ? _buildVerticalLayout() : _buildHorizontalLayout(); }, ), );}
~~~
OrientationBuilder 提供了 orientation 參數可以識別設備方向,而如果我們在 OrientationBuilder 之外,希望根據設備的旋轉方向設置一些組件的初始化行為,也可以使用 MediaQueryData 提供的 orientation 方法:
~~~
if(MediaQuery.of(context).orientation == Orientation.portrait) { //dosth}
~~~
需要注意的是,Flutter 應用默認支持豎屏和橫屏兩種模式。如果我們的應用程序不需要提供橫屏模式,也可以直接調用 SystemChrome 提供的 setPreferredOrientations 方法告訴 Flutter,這樣 Flutter 就可以固定視圖的布局方向了:
~~~
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
~~~
## 適配平板電腦
當適配更大的屏幕尺寸時,我們希望 App 上的內容可以適應屏幕上額外的可用空間。如果我們在平板中使用與手機相同的布局,就會浪費大量的可視空間。與適配屏幕旋轉類似,最直接的方法是為手機和平板電腦創建兩種不同的布局。然而,考慮到平板電腦和手機為用戶提供的功能并無差別,因此這種實現方式將會新增許多不必要的重復代碼。
為解決這個問題,我們可以采用另外一種方法:**將屏幕空間劃分為多個窗格,即采用與原生 Android、iOS 類似的 Fragment、ChildViewController 概念,來抽象獨立區塊的視覺功能。**
多窗格布局可以在平板電腦和橫屏模式上,實現更好的視覺平衡效果,增強 App 的實用性和可讀性。而,我們也可以通過獨立的區塊,在不同尺寸的手機屏幕上快速復用視覺功能。
如下圖所示,分別展示了普通手機、橫屏手機與平板電腦,如何使用多窗格布局來改造新聞列表和新聞詳情交互:
:-: 
圖 1 多窗格布局示意圖
首先,我們需要分別為新聞列表與新聞詳情創建兩個可重用的獨立區塊:
* 新聞列表,可以在元素被點擊時通過回調函數告訴父 Widget 元素索引;
* 而新聞詳情,則用于展示新聞列表中被點擊的元素索引。
對于手機來說,由于空間小,所以新聞列表區塊和新聞詳情區塊都是獨立的頁面,可以通過點擊新聞元素進行新聞詳情頁面的切換;而對于平板電腦(和手機橫屏布局)來說,由于空間足夠大,所以我們把這兩個區塊放置在同一個頁面,可以通過點擊新聞元素去刷新同一頁面的新聞詳情。
頁面的實現和區塊的實現是互相獨立的,通過區塊復用就可以減少編寫兩個獨立布局的工作:
~~~
// 列表 Widget
class ListWidget extends StatefulWidget {
final ItemSelectedCallback onItemSelected;
ListWidget(
this.onItemSelected,// 列表被點擊的回調函數
);
@override
_ListWidgetState createState() => _ListWidgetState();
}
class _ListWidgetState extends State<ListWidget> {
@override
Widget build(BuildContext context) {
// 創建一個 20 項元素的列表
return ListView.builder(
itemCount: 20,
itemBuilder: (context, position) {
return ListTile(
title: Text(position.toString()),// 標題為 index
onTap:()=>widget.onItemSelected(position),// 點擊后回調函數
);
},
);
}
}
// 詳情 Widget
class DetailWidget extends StatefulWidget {
final int data; // 新聞列表被點擊元素索引
DetailWidget(this.data);
@override
_DetailWidgetState createState() => _DetailWidgetState();
}
class _DetailWidgetState extends State<DetailWidget> {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.red,// 容器背景色
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(widget.data.toString()),// 居中展示列表被點擊元素索引
],
),
),
);
}
}
~~~
然后,我們只需要檢查設備屏幕是否有足夠的寬度來同時展示列表與詳情部分。為了獲取屏幕寬度,我們可以使用 MediaQueryData 提供的 size 方法。
在這里,我們將平板電腦的判斷條件設置為寬度大于 480。這樣,屏幕中就有足夠的空間可以切換到多窗格的復合布局了:
~~~
if(MediaQuery.of(context).size.width > 480) {
//tablet
} else {
//phone
}
~~~
最后,如果寬度夠大,我們就會使用 Row 控件將列表與詳情包裝在同一個頁面中,用戶可以點擊左側的列表刷新右側的詳情;如果寬度比較小,那我們就只展示列表,用戶可以點擊列表,導航到新的頁面展示詳情:
~~~
class _MasterDetailPageState extends State<MasterDetailPage> {
var selectedValue = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: OrientationBuilder(builder: (context, orientation) {
// 平板或橫屏手機,頁面內嵌列表 ListWidget 與詳情 DetailWidget
if (MediaQuery.of(context).size.width > 480) {
return Row(children: <Widget>[
Expanded(
child: ListWidget((value) {// 在列表點擊回調方法中刷新右側詳情頁
setState(() {selectedValue = value;});
}),
),
Expanded(child: DetailWidget(selectedValue)),
]);
} else {// 普通手機,頁面內嵌列表 ListWidget
return ListWidget((value) {// 在列表點擊回調方法中打開詳情頁 DetailWidget
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return Scaffold(
body: DetailWidget(value),
);
},
));
});
}
}),
);
}
}
~~~
運行一下代碼,可以看到,我們的應用已經完全適配不同尺寸、不同方向的設備屏幕了。
:-: 
圖 2 豎屏手機版列表詳情
:-: 
圖 3 橫屏手機版列表詳情
:-: 
圖 4 豎屏平板列表詳情
:-: 
圖 5 橫屏平板列表詳情
## 總結
好了,今天的分享就到這里。我們總結一下今天的核心知識點吧。
在 Flutter 中,為了適配不同設備屏幕,我們需要提供不同的布局方式。而將獨立的視覺區塊進行封裝,通過 OrientationBuilder 提供的 orientation 回調參數,以及 MediaQueryData 提供的屏幕尺寸,以多窗格布局的方式為它們提供不同的頁面呈現形態,能夠大大降低編寫獨立布局所帶來的重復工作。如果你的應用不需要支持設備方向,也可以通過 SystemChrome 提供的 setPreferredOrientations 方法,強制豎屏。
做好應用開發,我們除了要保證產品功能正常,還需要兼容碎片化(包括設備碎片化、品牌碎片化、系統碎片化、屏幕碎片化等方面)可能帶來的潛在問題,以確保良好的用戶體驗。
與其他維度碎片化可能帶來功能缺失甚至 Crash 不同,屏幕碎片化不至于導致功能完全不可用,但控件顯示尺寸卻很容易在沒有做好適配的情況下產生變形,讓用戶看到異形甚至不全的 UI 信息,影響產品形象,因此也需要重點關注。
在應用開發中,我們可以分別在不同屏幕尺寸的主流機型和模擬器上運行我們的程序,來觀察 UI 樣式和功能是否異常,從而寫出更加健壯的布局代碼。
我把今天分享所涉及到的知識點打包到了[GitHub](https://github.com/cyndibaby905/33_multi_screen_demo)中,你可以下載下來,反復運行幾次,加深理解與記憶。
## 思考題
最后,我給你留下一道思考題吧
setPreferredOrientations 方法是全局生效的,如果你的應用程序中有兩個相鄰的頁面,頁面 A 僅支持豎屏,頁面 B 同時支持豎屏和橫屏,你會如何實現呢?
- 前言
- 開篇詞
- 預習篇
- 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混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略