Flutter - 開發多種屏幕尺寸和方向(Fragments)

在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,以切換到第二種布局。

總結:

  1. 我們創建了兩個小部件,一個包含主列表,另一個包含詳細視圖。
  2. 我們創建兩個屏幕。在第一個屏幕上,我們檢查設備是否有足夠的寬度來處理這兩個小部件。
  3. 如果有足夠的寬度,我們在一個頁面上添加兩個小部件。如果沒有,我們在點擊列表項時導航到第二頁,該列表項只有詳細視圖。

我們來編碼吧

讓我們編寫我在本節頂部包含的演示代碼,其中我們有一個數字列表,詳細信息視圖顯示該數字。首先我們制作兩個小部件。

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),
    );
  }
}

它只在頁面上保存一個細節小部件,用于在較小的屏幕上顯示數據。

現在我們有一個功能正常的應用程序,適應不同大小和方向的屏幕。

一些更重要的事情

  1. 如果你想簡單地擁有不同的布局而沒有任何類似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文章,并對您對本文的任何反饋發表評論。

轉:https://medium.com/flutter-community/developing-for-multiple-screen-sizes-and-orientations-in-flutter-fragments-in-flutter-a4c51b849434

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容