解密Flutter響應式佈局android
Flutter是一個跨平臺的應用開發框架,支持各類屏幕大小的設備,它能夠在智能手錶這樣的小設備上運行,也能夠在電視這樣的大設備上運行。使用相同的代碼來適應不一樣的屏幕大小和像素密度是一個挑戰。ios
Flutter響應式佈局的設計沒有硬性的規則。在本文中,我將向您展現在設計響應式佈局時能夠遵循的一些方法。git
在使用Flutter構建響應式佈局以前,我想說明一下Android和iOS是如何處理不一樣屏幕大小的佈局的。github
爲了處理不一樣的屏幕尺寸和像素密度,在Android中使用瞭如下概念:app
Android UI設計中引入的一個革命性的東西是ConstraintLayout。它能夠用於建立靈活的、響應性強的UI設計,以適應不一樣的屏幕大小和尺寸。它容許您根據與佈局中其餘視圖的空間關係來指定每一個視圖的位置和大小。框架
但這並不能解決大型設備的問題,在大型設備中,拉伸或只是調整UI組件的大小並非利用屏幕面積的最優雅的方式。在屏幕面積很小的智能手錶,調整組件以適應屏幕大小可能會致使奇怪的UI。less
要解決上述問題,您能夠爲不一樣大小的設備使用alternative layouts。例如,你能夠在平板電腦等設備上使用分屏視圖來提供良好的用戶體驗,並明智地使用大屏幕。ide
在Android中,你能夠爲不一樣的屏幕大小定義不一樣的佈局文件,Android框架會根據設備的屏幕大小自動處理這些佈局之間的切換。函數
使用Fragment,你能夠將你的UI邏輯提取到單獨的組件中,這樣當你爲大屏幕尺寸設計多窗格佈局時,你沒必要單獨定義邏輯。您能夠重用爲每一個片斷定義的Fragment。佈局
Vector graphics使用XML建立圖像來定義路徑和顏色,而不是使用像素位圖。它能夠縮放到任何大小。在Android中,你可使用VectorDrawable來繪製任何類型的插圖,好比圖標。
iOS用於定義響應式佈局的方式以下
Auto Layout可用於構建自適應界面,您能夠在其中定義用於控制應用程序內容的規則(稱爲約束)。 當檢測到某些環境變化(稱爲特徵)時,「Auto Layout」會根據指定的約束條件自動從新調整佈局。
Size類的特色是會根據其大小自動分配給內容區域。 iOS 會根據內容區域的Size類別動態地進行佈局調整。在iPad上,size類也適用。
還有一些其餘的UI嘴賤你能夠用來在iOS上構建響應式UI,像UIStackView, UIViewController,和UISplitViewController。
即便你不是Android或iOS的開發者,到目前爲止,你應該已經瞭解了這些平臺是如何處理響應式佈局的。
在Android中,要在單個屏幕上顯示多個UI視圖,請使用Fragments,它們相似於可在應用程序的Activity中運行的可重用組件。
您能夠在一個Activity中運行多個Fragment,可是不能在一個應用程序中同時運行多個Activity。
在iOS中,爲了控制多個視圖控制器,使用了UISplitViewController,它在分層界面中管理子視圖控制器。
如今咱們來到Flutter
Flutter引入了widget的概念。它們像積木同樣拼湊在一塊兒構建應用程序畫面。
記住,在Flutter中,每一個屏幕和整個應用程序也是一個widget!
widget本質上是可重用的,所以在Flutter中構建響應式佈局時,您不須要學習任何其餘概念。
正如我前面所說的,我將討論開發響應式佈局所需的重要概念,而後你來選擇使用什麼樣的方式在你的APP上實現響應式佈局。
你可使用MediaQuery來檢索屏幕的大小(寬度/高度)和方向(縱向/橫向)。
下面是例子
class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { Size screenSize = MediaQuery.of(context).size; Orientation orientation = MediaQuery.of(context).orientation; return Scaffold( body: Container( color: CustomColors.android, child: Center( child: Text( 'View\n\n' + '[MediaQuery width]: ${screenSize.width.toStringAsFixed(2)}\n\n' + '[MediaQuery orientation]: $orientation', style: TextStyle(color: Colors.white, fontSize: 18), ), ), ), ); } }
使用LayoutBuilder類,您能夠得到BoxConstraints對象,該對象可用於肯定小部件的maxWidth和maxHeight。
請記住:MediaQuery和LayoutBuilder之間的主要區別在於,MediaQuery使用屏幕的完整上下文,而不只僅是特定小部件的大小。而LayoutBuilder能夠肯定特定小部件的最大寬度和高度。
下面是例子
class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { Size screenSize = MediaQuery.of(context).size; return Scaffold( body: Row( children: [ Expanded( flex: 2, child: LayoutBuilder( builder: (context, constraints) => Container( color: CustomColors.android, child: Center( child: Text( 'View 1\n\n' + '[MediaQuery]:\n ${screenSize.width.toStringAsFixed(2)}\n\n' + '[LayoutBuilder]:\n${constraints.maxWidth.toStringAsFixed(2)}', style: TextStyle(color: Colors.white, fontSize: 18), ), ), ), ), ), Expanded( flex: 3, child: LayoutBuilder( builder: (context, constraints) => Container( color: Colors.white, child: Center( child: Text( 'View 2\n\n' + '[MediaQuery]:\n ${screenSize.width.toStringAsFixed(2)}\n\n' + '[LayoutBuilder]:\n${constraints.maxWidth.toStringAsFixed(2)}', style: TextStyle(color: CustomColors.android, fontSize: 18), ), ), ), ), ), ], ), ); } }
PS:當你在構建一個小部件,想知道他的寬度是多少時,使用這個組件,你能夠根據子組件可用高/寬度來進行判斷,構建不一樣的佈局
要肯定widget的當前方向,可使用OrientationBuilder類。
記住:這與你使用MediaQuery檢索的設備方向不一樣。
下面是例子
class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { Orientation deviceOrientation = MediaQuery.of(context).orientation; return Scaffold( body: Column( children: [ Expanded( flex: 2, child: Container( color: CustomColors.android, child: OrientationBuilder( builder: (context, orientation) => Center( child: Text( 'View 1\n\n' + '[MediaQuery orientation]:\n$deviceOrientation\n\n' + '[OrientationBuilder]:\n$orientation', style: TextStyle(color: Colors.white, fontSize: 18), ), ), ), ), ), Expanded( flex: 3, child: OrientationBuilder( builder: (context, orientation) => Container( color: Colors.white, child: Center( child: Text( 'View 2\n\n' + '[MediaQuery orientation]:\n$deviceOrientation\n\n' + '[OrientationBuilder]:\n$orientation', style: TextStyle(color: CustomColors.android, fontSize: 18), ), ), ), ), ), ], ), ); } }
portrait (縱向) landscape(橫向)
PS:看了下OrientationBuilder的源碼註釋
widget的方向僅僅是其寬度相對於高度的一個係數。若是一個[Column]部件的寬度超過了它的高度,它的方向是橫向的,即便它以垂直的形式顯示其子元素。
這是譯者的代碼
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; /// Copyright (C), 2020-2020, flutter_demo /// FileName: orientationBuilder_demo /// Author: Jack /// Date: 2020/12/6 /// Description: class OrientationBuilderDemo extends StatelessWidget { @override Widget build(BuildContext context) { Orientation deviceOrientation = MediaQuery.of(context).orientation; return Scaffold( body: Column( children: [ Expanded( flex: 1, child: Container( color: Colors.greenAccent, child: OrientationBuilder( builder: (context, orientation) => Center( child: Text( 'View 1\n\n' + '[MediaQuery orientation]:\n$deviceOrientation\n\n' + '[OrientationBuilder]:\n$orientation', style: TextStyle(color: Colors.white, fontSize: 18), ), ), ), ), ), Expanded( flex: 2, child: OrientationBuilder( builder: (context, orientation) => Container( color: Colors.white, child: Center( child: Text( 'View 2\n\n' + '[MediaQuery orientation]:\n$deviceOrientation\n\n' + '[OrientationBuilder]:\n$orientation', style: TextStyle(color: Colors.greenAccent, fontSize: 18), ), ), ), ), ), ], ), ); } }
想必你已經理解了OrientationBuilder的方向定義,若是一個小部件的寬大於高,他就是橫向的,若是高大於寬,他就是橫向的,僅此而已。
在Row或Column中特別有用的小部件是 Expanded 和 Flexible。當Expanded 使用在一個Row、Column或Flex中,Expanded 可使它的子Widget自動填充可用空間,與之相反,Flexible 的子widget不會填滿整個可用空間。
例子以下。
class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, body: SafeArea( child: Column( children: [ Row( children: [ ExpandedWidget(), FlexibleWidget(), ], ), Row( children: [ ExpandedWidget(), ExpandedWidget(), ], ), Row( children: [ FlexibleWidget(), FlexibleWidget(), ], ), Row( children: [ FlexibleWidget(), ExpandedWidget(), ], ), ], ), ), ); } } class ExpandedWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Expanded( child: Container( decoration: BoxDecoration( color: CustomColors.android, border: Border.all(color: Colors.white), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Text( 'Expanded', style: TextStyle(color: Colors.white, fontSize: 24), ), ), ), ); } } class FlexibleWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Flexible( child: Container( decoration: BoxDecoration( color: CustomColors.androidAccent, border: Border.all(color: Colors.white), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Text( 'Flexible', style: TextStyle(color: CustomColors.android, fontSize: 24), ), ), ), ); } }
PS:與[expand]不一樣的是,[Flexible]不須要子widget填充剩餘的空間,第一個例子,expanded雖然有填充空餘空間的功能,不過expanded組件和flexible組件的flex都是1,至關於將縱軸分紅兩半,expanded所擁有的所有空間就是縱軸的一半,實際他已經填充了。
FractionallySizedBox widget將其子元素的大小調整爲可用空間的一小部分。它在Expanded 或Flexible widget中特別有用。
class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, body: SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ FractionallySizedWidget(widthFactor: 0.4), ], ), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ FractionallySizedWidget(widthFactor: 0.6), ], ), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ FractionallySizedWidget(widthFactor: 0.8), ], ), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ FractionallySizedWidget(widthFactor: 1.0), ], ), ], ), ), ); } } class FractionallySizedWidget extends StatelessWidget { final double widthFactor; FractionallySizedWidget({@required this.widthFactor}); @override Widget build(BuildContext context) { return Expanded( child: FractionallySizedBox( alignment: Alignment.centerLeft, widthFactor: widthFactor, child: Container( decoration: BoxDecoration( color: CustomColors.android, border: Border.all(color: Colors.white), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Text( '${widthFactor * 100}%', style: TextStyle(color: Colors.white, fontSize: 24), ), ), ), ), ); } }
PS:當你想讓你的widget,佔據當前屏幕寬度和高度的百分之多少時,使用這個組件,想在Row和Column組件中使用百分比佈局時,須要在FractionallySizedBox外包裹一個expanded或flexible
可使用AspectRatio小部件將子元素的大小調整爲特定的長寬比。首先,它嘗試佈局約束容許的最大寬度,並經過將給定的高寬比應用於寬度來決定高度。
class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, body: SafeArea( child: Column( children: [ AspectRatioWidget(ratio: '16 / 9'), AspectRatioWidget(ratio: '3 / 2'), ], ), ), ); } } class AspectRatioWidget extends StatelessWidget { final String ratio; AspectRatioWidget({@required this.ratio}); @override Widget build(BuildContext context) { return AspectRatio( aspectRatio: Fraction.fromString(ratio).toDouble(), child: Container( decoration: BoxDecoration( color: CustomColors.android, border: Border.all(color: Colors.white), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Center( child: Text( 'AspectRatio - $ratio', style: TextStyle(color: Colors.white, fontSize: 24), ), ), ), ), ); } }
咱們已經研究了大多數重要的概念,爲創建一個響應式佈局Flutter app,除了最後一個。
在構建一個示例響應式應用程序時,讓咱們學習最後一個概念。
如今,咱們將應用上一節中描述的一些概念。與此同時,您還將學習爲大屏幕構建佈局的另外一個重要概念,即分屏視圖(一個屏幕上顯示多個頁面)。
響應式佈局:在不一樣大小的屏幕上使用不一樣的佈局。
咱們將創建一個名叫Flow的聊天應用程序。
app主要由兩個部分組成:
HomePage (PeopleView
, BookmarkView
, ContactView
)
ChatPage (PeopleView
, ChatView
)
對於大屏幕,咱們將顯示包含MenuWidget和DestinationView的分屏視圖。您能夠看到,在Flutter中建立分屏視圖是很是容易的,您只需使用一行將它們並排放置,而後爲了填滿整個空間,只需使用Expanded widget包裝兩個視圖。您還能夠定義擴展小部件的flex屬性,這將容許您指定每一個小部件應該覆蓋屏幕的多少部分(默認flex設置爲1)。
可是,若是您如今移動到一個特定的屏幕,而後在視圖之間切換,那麼您將丟失頁面的上下文,也就是說您將始終返回到第一個頁面,即「聊天」。爲了解決這個問題,我使用了多個回調函數來返回所選頁面到主頁。實際上,您應該使用狀態管理技術來處理此場景。因爲本文的惟一目的是教您構建響應式佈局,因此我不討論任何狀態管理的複雜性。
響應式APP Github地址感興趣的小夥伴能夠看一看
上文中的全部佈局組件的demo都在譯者的github上
地址https://github.com/jack0-0wu/flutter_demo
本文爲medium翻譯
原文地址
https://medium.com/flutter-community/demystifying-responsive-layout-in-flutter-f85d0014b94e
坐而論道不如起而行之