在Google的Flutter Mobile SDK中制作自適應屏幕
移動應用需要支持各種器件尺寸,像素密度和方向。應用程序需要能夠很好地擴展,處理方向更改并通過所有這些來持久保存數據。Flutter使您能夠選擇應對這些挑戰的方式,而不是僅提供一個特定的解決方案。
解決大屏幕的Android解決方案
在Android中,我們處理更大的屏幕,例如具有備用布局文件的平板電腦,我們可以定義最小寬度和橫向/縱向方向。
這意味著我們必須為手機定義一個布局文件,一個用于平板電腦,然后為每種設備類型定義兩個方向。然后根據運行它的設備實例化這些布局。然后我們檢查哪個布局是活動的(移動/平板電腦)并相應地初始化。
對于大多數應用程序,使用master-detail流處理更大的屏幕大小(使用片段)。稍后我們將詳細討論master-detail流是什么。
Android中的Fragments本質上是可重用的組件,可以在屏幕中使用。Fragments有自己的布局和Java / Kotlin類來控制數據和片段的生命周期。這是一項相當大的工作,需要大量代碼才能開始工作。
我們先來看看處理方向,然后處理Flutter的屏幕尺寸。
在Flutter中使用方向
當我們使用方向時,我們希望使用屏幕的整個寬度并顯示可能的最大信息量。
下面的示例在兩個方向中創建一個基本的配置文件頁面,并根據方向不同地構建布局,以最大限度地使用屏幕寬度。完整的源代碼將托管在GitHub上(本文末尾給出的鏈接)。
在這里,我們有一個簡單的屏幕,具有不同的縱向和橫向布局。讓我們嘗試通過創建上面的示例來了解我們如何在Flutter中實際切換布局。
我們該如何解決這個問題?
在概念上,我們的工作方式非常類似于Android的做事方式。我們有兩個布局(不是布局文件,因為Flutter沒有布局文件),一個用于縱向,一個用于橫向。當設備改變方向時,我們重建我們的布局。
我們如何檢測方向變化?
首先,我們使用一個名為OrientationBuilder的小部件。OrientationBuilder是一個小部件,可在方向更改時構建布局或布局的一部分。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: OrientationBuilder(
builder: (context, orientation) {
return orientation == Orientation.portrait
? _buildVerticalLayout()
: _buildHorizontalLayout();
},
),
);
}
OrientationBuilder有一個構建器函數來構建布局。當方向改變時,將調用builder函數。方向的值:Orientation.portrait或Orientation.landscape。
在這個例子中,我們檢查屏幕是否處于縱向模式并構建垂直布局(如果是這種情況),否則我們為屏幕構建水平布局。
_buildVerticalLayout()和_buildHorizo??ntalLayout()是我編寫的用于創建相應布局的方法。
我們還可以使用代碼檢查代碼中的任何位置(OrientationBuilder內部或外部)的方向
MediaQuery.of(context).orientation
注意:在我們懶惰和或只有portrait的時候,請使用
SystemChrome.setPreferredOrientations(DeviceOrientation.portraitUp);
在Flutter中為更大的屏幕創建布局
當我們處理更大的屏幕尺寸時,我們希望我們的屏幕適應使用屏幕上的可用空間。最直接的方法是為平板電腦和手機創建兩種不同的布局甚至屏幕。(這里,“布局”表示屏幕的可視部分。“屏幕”指的是布局和連接到它的所有后端代碼。)然而,這涉及許多不必要的代碼,并且代碼需要重復。
那么我們如何解決這個問題呢?
首先,讓我們來看看它最常見的用例。
讓我們回到我們要討論的“Master-Detail Flow”。對于應用程序,您將看到一個常見模式,其中您有一個Master項目列表,當您單擊列表項時,您將被重定向到另一個Detail屏幕。以Gmail為例,我們有一個電子郵件列表,當我們點擊其中一個時,會打開一個詳細視圖,其中包含郵件內容。
讓我們為這個流程做一個示例應用程序。
移動縱向模式下的主 - 細節流程
此應用程序只保存一個數字列表,并在點擊時突出顯示一個數字。我們有一個主數字列表和一個詳細視圖,在點擊時顯示一個數字。就像電子郵件一樣。
如果我們在平板電腦中使用相同的布局,那將是一個相當大的空間浪費。那么我們可以做些什么來解決它呢?我們可以在同一屏幕上同時擁有主列表和詳細視圖,因為我們有可用的屏幕空間。
平板電腦橫向模式下的Master-Detail Flow
那么我們可以做些什么來減少編寫兩個獨立屏幕的工作呢?
讓我們看看Android是如何解決這個問題的。Android從主列表和詳細信息視圖中創建稱為Fragments的可重用組件。Fragments可以與屏幕分開定義,只是添加到屏幕中而不重復兩次代碼。
因此Fragments A是主列表片段,B是細節片段。在移動設備或較小寬度的布局中,單擊列表項會導航到單獨的頁面,而在平板電腦中,它將保留在同一頁面上并更改詳細信息片段。當手機旋轉到風景時,我們也可以做類似平板電腦的界面。
這就是Flutter的力量所在。
Flutter中的每個小部件都是天生的,可重用的。
Flutter中的每個小部件都像一個Fragments。
我們需要做的就是定義兩個小部件。一個用于主列表,一個用于詳細視圖。實際上,這些是碎片。我們只需檢查設備是否有足夠的寬度來處理列表和細節部分。如果是,我們使用兩個小部件。如果設備沒有足夠的寬度來支持兩者,我們只顯示列表并導航到單獨的屏幕以顯示詳細內容。
我們首先需要檢查設備的寬度,看看我們是否可以使用更大的布局而不是更小的布局。為了獲得寬度,我們使用
MediaQuery.of(context).size.width
尺寸以dps為單位給出了設備的高度和寬度。
讓我們將最小寬度設置為600 dp,以切換到第二種布局。
總結:
- 我們創建了兩個小部件,一個包含主列表,另一個包含詳細視圖。
- 我們創建兩個屏幕。在第一個屏幕上,我們檢查設備是否有足夠的寬度來處理這兩個小部件。
- 如果有足夠的寬度,我們在一個頁面上添加兩個小部件。如果沒有,我們在點擊列表項時導航到第二頁,該列表項只有詳細視圖。
我們來編碼吧
讓我們編寫我在本節頂部包含的演示代碼,其中我們有一個數字列表,詳細信息視圖顯示該數字。首先我們制作兩個小部件。
List Widget(List Fragment)
typedef Null ItemSelectedCallback(int value);
class ListWidget extends StatefulWidget {
final int count;
final ItemSelectedCallback onItemSelected;
ListWidget(
this.count,
this.onItemSelected,
);
@override
_ListWidgetState createState() => _ListWidgetState();
}
class _ListWidgetState extends State<ListWidget> {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: widget.count,
itemBuilder: (context, position) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
child: InkWell(
onTap: () {
widget.onItemSelected(position);
},
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(position.toString(), style: TextStyle(fontSize: 22.0),),
),
],
),
),
),
);
},
);
}
}
在列表中,我們會顯示要顯示的項目數以及單擊項目時的回調。此回調非常重要,因為它決定是在簡單的屏幕上更改詳細視圖還是在較小的屏幕上導航到不同的頁面。
我們只是為每個索引顯示卡片并用InkWell包圍它以響應點擊。
細節小部件(細節片段)
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.blue,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(widget.data.toString(), style: TextStyle(fontSize: 36.0, color: Colors.white),),
],
),
),
);
}
}
細節小部件只需一個數字并顯著地顯示它。
請注意,這些不是屏幕。這些只是我們將在屏幕上使用的小部件。
主屏幕
class MasterDetailPage extends StatefulWidget {
@override
_MasterDetailPageState createState() => _MasterDetailPageState();
}
class _MasterDetailPageState extends State<MasterDetailPage> {
var selectedValue = 0;
var isLargeScreen = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: OrientationBuilder(builder: (context, orientation) {
if (MediaQuery.of(context).size.width > 600) {
isLargeScreen = true;
} else {
isLargeScreen = false;
}
return Row(children: <Widget>[
Expanded(
child: ListWidget(10, (value) {
if (isLargeScreen) {
selectedValue = value;
setState(() {});
} else {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return DetailPage(value);
},
));
}
}),
),
isLargeScreen ? Expanded(child: DetailWidget(selectedValue)) : Container(),
]);
}),
);
}
}
這是應用程序的主頁面。我們有兩個變量:selectedValue用于存儲選定的列表項,isLargeScreen是一個簡單的布爾值,用于存儲屏幕是否足夠大以顯示列表和詳細信息小部件。
我們周圍還有一個OrientationBuilder,因此如果手機被旋轉到橫向模式并且它有足夠的寬度來顯示兩個元素,那么它將以這種方式重建。
我們首先檢查寬度是否足夠大以顯示我們的布局
if (MediaQuery.of(context).size.width > 600) {
isLargeScreen = true;
} else {
isLargeScreen = false;
}
代碼的主要部分是:
isLargeScreen ? Expanded(child: DetailWidget(selectedValue)) : Container(),
如果屏幕很大,我們會添加一個細節小部件,如果不是,我們會返回一個空容器。我們使用它周圍的擴展小部件來填充屏幕,或者在屏幕較大的情況下將屏幕分成比例。因此,Expanded允許每個小部件通過設置Flex屬性來填充屏幕的一半甚至一定百分比。
第二個重要部分是:
if (isLargeScreen) {
selectedValue = value;
setState(() {});
} else {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return DetailPage(value);
},
));
}
這意味著,如果使用更大的布局,我們不需要轉到不同的屏幕,因為細節小部件在頁面本身上。如果屏幕較小,我們需要導航到不同的頁面,因為只有列表顯示在當前屏幕上。
最后,
詳細頁面(適用于較小的屏幕)
class DetailPage extends StatefulWidget {
final int data;
DetailPage(this.data);
@override
_DetailPageState createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: DetailWidget(widget.data),
);
}
}
它只在頁面上保存一個細節小部件,用于在較小的屏幕上顯示數據。
現在我們有一個功能正常的應用程序,適應不同大小和方向的屏幕。
一些更重要的事情
- 如果你想簡單地擁有不同的布局而沒有任何類似Fragment的布局,你可以簡單地在build方法中編寫
if (MediaQuery.of(context).size.width > 600) {
isLargeScreen = true;
} else {
isLargeScreen = false;
}
return isLargeScreen? _buildTabletLayout() : _buildMobileLayout();
并編寫兩種方法來構建您的布局。
2.如果您只想設計平板電腦設計,而不是檢查MediaQuery的寬度,請獲取尺寸并使用它來獲取實際寬度而不是特定方向的寬度。當我們直接使用MediaQuery的寬度時,它將獲得僅在該方向上獲得寬度。因此在橫向模式下,手機的長度被視為寬度。
Size size = MediaQuery.of(context).size;
double width = size.width > size.height ? size.height : size.width;
if(width > 600) {
// Do something for tablets here
} else {
// Do something for phones
}
Github鏈接本文中的示例:
https://github.com/deven98/FlutterAdaptiveLayouts
就是這篇文章!我希望你喜歡它并留下一些鼓掌,如果你這樣做。請關注我以獲取更多Flutter文章,并對您對本文的任何反饋發表評論。