Flutter之支持不一樣的屏幕尺寸和方向

介紹

本文是medium的一篇文章的翻譯,再加上本身的一點理解,已獲得做者的贊成。android

主要講的是在平板和手機中,處理適配不一樣屏幕的問題。git

原文地址:medium.com/flutter-community/developing-for-multiple-screen-sizes-a nd-orientations-in-flutter-fragments-in-flutter-a4c51b849434github

Android中解決大屏幕的方法

在Android中,咱們處理比較大尺寸的屏幕,例如平板電腦。咱們能夠用過最小寬度限定符來定義相應尺寸的佈局文件的名稱。 bash

https://user-gold-cdn.xitu.io/2019/1/21/1687026ab4e76109?w=701&h=472&f=png&s=150564
這意味着咱們要去給一個佈局文件用於手機,一個佈局用於平板電腦。而後在運行的時候,根據設備,實例化相應的佈局。而後咱們要去檢查哪一個佈局是active的,並進行相應的初始化操做。官方關於Android支持不一樣屏幕尺寸的文檔: developer.android.com/training/mu…

Android中的Fragment本質上是可重用的組件,能夠在屏幕中使用。Fragment有本身的佈局文件,而且 Java/Kotlin的類會去控制Fragment的生命週期。這是一項至關大的工做,須要大量代碼才能開始工做。app

下面,咱們先來看看在Flutter中處理屏幕方向,而後處理Flutter的屏幕尺寸。less

在Flutter中使用方向

當咱們使用屏幕方向的時候,咱們但願使用屏幕的所有寬度和顯示儘量的最大信息量。ide

下面的示例,在兩個方向上建立一個基本的配置文件頁面,並根據不一樣的方向去構建佈局,以最大限度地使用屏幕寬度。 函數

https://user-gold-cdn.xitu.io/2019/1/21/1687026ac85e885d?w=1080&h=1920&f=gif&s=2215836
在這裏,咱們有一個簡單的屏幕,具備不一樣的縱向和橫向的佈局。下面,咱們嘗試經過建立上面的示例來了解 如何在Flutter中實現橫豎屏切換佈局。

如何解決這個問題

在概念上,解決方法跟Android的方法是相似的。咱們也要弄兩個佈局(這裏的佈局並非Android中的佈局文件,由於在Flutter中 沒有佈局文件),一個用於縱向,一個用於橫向。而後當設備的方向改變的時候,rebuild更新咱們的佈局。佈局

如何檢測方向變化

Flutter中提供了一個OrientationBuilder的小部件。OrientationBuilder能夠在設備的方向發生改變的時候,從新構建佈局。ui

typedef OrientationWidgetBuilder = Widget Function(BuildContext context, Orientation orientation);
class OrientationBuilder extends StatelessWidget {
  /// Creates an orientation builder.
  const OrientationBuilder({
    Key key,
    @required this.builder,
  }) : assert(builder != null),
       super(key: key);

  /// Builds the widgets below this widget given this widget's orientation. /// A widget's orientation is simply a factor of its width relative to its
  /// height. For example, a [Column] widget will have a landscape orientation
  /// if its width exceeds its height, even though it displays its children in
  /// a vertical array.
  final OrientationWidgetBuilder builder;

  Widget _buildWithConstraints(BuildContext context, BoxConstraints constraints) {
    // If the constraints are fully unbounded (i.e., maxWidth and maxHeight are
    // both infinite), we prefer Orientation.portrait because its more common to
    // scroll vertically then horizontally.
    final Orientation orientation = constraints.maxWidth > constraints.maxHeight ? Orientation.landscape : Orientation.portrait;
    return builder(context, orientation);
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: _buildWithConstraints);
  }
}
複製代碼

OrientationBuilder有一個builder函數來構建咱們的佈局。當設備的方向發生改變的時候,就會調用 builder函數。orientation的值有兩個,Orientation.landscape和Orientation.portrait。

@override
Widget build(BuildContext context){
  return Scaffold(
    appBar:AppBar(),
    body:OrientationBuilder(
      builder :( context,orientation){
        return orientation == Orientation. portrait
            ?_buildVerticalLayout()
            :_ buildHorizo​​ntalLayout();
      },
    ),
  );
}
複製代碼

在上面的例子中,咱們檢查屏幕是否處於豎屏模式並構建豎屏的佈局,不然咱們爲屏幕構建橫屏的佈局。 _buildVerticalLayout()和_buildHorizontalLayout()是編寫的用於建立相應佈局的方法。

在Flutter中建立更大屏幕的佈局

當咱們處理更大的屏幕尺寸的時候,咱們但願屏幕適應地去使用屏幕上的可用空間。最直接的方法就是爲平板電腦 和手機建立兩種不一樣的佈局(這裏的的佈局,表示屏幕的可視部分)。然而,這裏會涉及許多沒必要要的代碼,而且代碼須要被重用。

那麼咱們如何解決這個問題呢?

首先,讓咱們來看看它最多見的用例。

這裏討論下「Master-Detail Flow」。對於應用程序來講,你會看到一個常見的場景。其中有一個Master的列表,而後當你點擊列表項Item的時候,就會跳到另外一個顯示Detail詳細信息的屏幕。以Gmail爲例,咱們有一個電子郵件的列表,當咱們點擊其中一個的時候, 會打開一個顯示詳細信息的頁面,其中包含郵件內容。

https://user-gold-cdn.xitu.io/2019/1/21/1687026aa5c9c547?w=720&h=526&f=png&s=142668
讓咱們爲這個流程作一個示例的應用程序。
https://user-gold-cdn.xitu.io/2019/1/21/1687026acd0b488f?w=1080&h=1920&f=gif&s=3447249
這個應用程序只保存一個數字列表,並在點擊的時候,跳轉到只顯示一個數字的詳細視圖。

若是咱們在平板電腦中使用相同的佈局,那將是一個至關大的空間浪費。那麼咱們能夠作些什麼來解決它呢? 咱們能夠在同一屏幕上同時擁有主列表和詳細視圖,由於咱們有足夠可用的屏幕空間。

https://user-gold-cdn.xitu.io/2019/1/21/1687026ac99d7a32?w=2048&h=1536&f=gif&s=773243
那麼咱們能夠作些什麼,來減小編寫兩個獨立屏幕的工做呢?

先看看Android中是如何解決這個問題的。Android從主列表和詳細信息視圖中建立稱爲Fragment的可重用組件。 Fragment能夠與屏幕分開定義,只是簡單地將fragment添加到屏幕中而不是重複的兩套代碼。

https://user-gold-cdn.xitu.io/2019/1/21/1687026aa4c69f51?w=586&h=229&f=png&s=34659
所以FragmentA是顯示主列表的fragment,FragmentB是顯示詳細信息的fragment。在較小寬度的佈局中,單擊列表項Item,會導航到單獨的頁面來顯示詳細視圖FragmentB, 而在平板電腦中,fragmentB將跟fragmentA顯示在同一屏幕上。

This is where the power of Flutter comes in.

Every widget in Flutter is by nature, reusable.

Every widget in Flutter is like a Fragment.

咱們須要作的就是定義兩個Widget,一個用於顯示主列表,一個用於顯示詳細視圖。實際上,這些就是相似的fragments。

咱們只須要檢查設備是否具備足夠的寬度來處理列表視圖和詳細視圖。若是是,咱們在同一屏幕上顯示兩個widget。若是設備沒有足夠的寬度來包含兩個界面,那咱們只須要在屏幕中展現主列表,點擊列表項後導航到獨立的屏幕來顯示詳細視圖。

首先,咱們須要檢查設備的寬度,看看咱們是否可使用更大的佈局,而不是使用更小的佈局。 爲了得到寬度,咱們使用MediaQuery來獲取寬度,Size中的寬度和高度的單位是dp。

MediaQuery.of(context).size.width
複製代碼

讓咱們將最小寬度設置爲600dp,以切換到第二種佈局。

總結:

  1. 咱們建立了兩個Widget,一個包含主列表,一個顯示詳細視圖
  2. 咱們建立了兩個屏幕,在第一個屏幕上,咱們檢查設備是否具備足夠的寬度來處理這兩個小部件
  3. 若是具備足夠的寬度,咱們將在第一個頁面上添加上兩個Widget就好了。若是沒有,咱們在第一個頁面只添加主列表Widget, 在點擊列表項以後導航到第二個屏幕上,顯示詳細視圖的Widget。

代碼實現

代碼的實現我是用本身的代碼進行說明,跟原做者的代碼實現的思路和結果是同樣的。 下面實現的是,有一個數字列表,點擊後顯示詳細視圖。

ListWidget

https://user-gold-cdn.xitu.io/2019/1/21/1687026b765a0534?w=632&h=930&f=png&s=10797

//須要定義一個回調,決定是在同一個屏幕上顯示更改詳細視圖仍是在較小的屏幕上導航到不一樣界面。
typedef Null ItemSelectedCallback(int value);
//列表的Widget
class ListWidget extends StatelessWidget {
  ItemSelectedCallback itemSelectedCallback;

  ListWidget({@required this.itemSelectedCallback});

  @override
  Widget build(BuildContext context) {
    return new ListView.builder(
        itemCount: 20,
        itemBuilder: (context, index) {
          return new ListTile(
            title: new Text("$index"),
            onTap: () {
            //設置點擊事件
              this.itemSelectedCallback(index);
            },
          );
        });
  }
}
複製代碼

DetailWidget

https://user-gold-cdn.xitu.io/2019/1/21/1687026ba708fdc1?w=630&h=930&f=png&s=3399

//詳細視圖的Widget,簡單的顯示一個文本
class DetailWidget extends StatelessWidget {
  final int data;

  DetailWidget(this.data);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        color: Colors.blue,
        child: new Center(
          child: new Text("詳細視圖:$data"),
        ),
      ),
    );
  }
}
複製代碼

請注意,這些Widget不是屏幕,只是咱們將在屏幕上使用的小部件。

主屏幕

https://user-gold-cdn.xitu.io/2019/1/21/1687026bbc94b431?w=720&h=488&f=png&s=7025

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool isLargeScreen; //是不是大屏幕

  var selectValue = 0; //保存選擇的內容

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new OrientationBuilder(builder: (context, orientation) {
        print("width:${MediaQuery.of(context).size.width}");
        //判斷屏幕寬度
        if (MediaQuery.of(context).size.width > 600) {
          isLargeScreen = true;
        } else {
          isLargeScreen = false;
        }
        //兩個widget是放在一個Row中進行顯示,若是是小屏幕的話,用一個空的Container進行佔位
        //若是是大屏幕的話,則用Expanded進行屏幕的劃分並顯示詳細視圖
        return new Row(
          mainAxisSize: MainAxisSize.max,
          children: <Widget>[
            new Expanded(child: new ListWidget(
              itemSelectedCallback: (value) {
                //定義列表項的點擊回調
                if (isLargeScreen) {
                  selectValue = value;
                  setState(() {});
                } else {
                  Navigator.of(context)
                      .push(new MaterialPageRoute(builder: (context) {
                    return new DetailWidget(value);
                  }));
                }
              },
            )),
            isLargeScreen
                ? new Expanded(child: new DetailWidget(selectValue))
                : new Container()
          ],
        );
      }),
    );
  }
}
複製代碼

這是應用程序的主頁面。有兩個變量:selectedValue用於存儲選定的列表項,isLargeScreen表示屏幕是否足夠大。

這裏還用了OrientatinBuilder包裹在最外面,因此當若是手機被旋轉到橫屏的時候,而且有足夠的寬度來顯示兩個Widget的話,那它將以這種方式重建。(若是不須要這功能,那能夠把OrientatinBuilder去掉就行)。

  • 代碼的主要部分是:
isLargeScreen
                ? new Expanded(child: new DetailWidget(selectValue))
                : new Container()
複製代碼

若是isLargeScreen爲true,則添加一個Expanded控件內部包裹DetailWidget。 Expanded容許每一個小部件經過設置Flex屬性來填充屏幕。

若是isLargeScreen爲false,則返回一個空的Container就好了,ListWidget所在的Expanded會自動填充滿屏幕。

  • 第二個重要部分是:
//定義列表項的點擊回調
                if (isLargeScreen) {
                  selectValue = value;
                  setState(() {});
                } else {
                  Navigator.of(context)
                      .push(new MaterialPageRoute(builder: (context) {
                    return new DetailWidget(value);
                  }));
                }
複製代碼

定義列表項的點擊回調,若是屏幕較小,咱們須要導航到不一樣的頁面。若是屏幕較大,就不須要導航到不一樣的屏幕,由於DetailWidget就在這個屏幕裏面,只需調用setState()去刷新界面就行。

如今咱們有一個功能正常的應用程序,可以適應不一樣大小的屏幕和方向。

https://user-gold-cdn.xitu.io/2019/1/21/1687026bcf3e4095?w=720&h=809&f=png&s=11876

一些更重要的事情

  1. 若是隻是想簡單地擁有不一樣的佈局(按照個人理解,就是橫屏豎屏的佈局是徹底不同的,不須要去複用一部分代碼)而沒有相似Fragment的佈局,那能夠只能夠直接在build方法中編寫不一樣的方法進行構建就行。
if (MediaQuery.of(context).size.width > 600) {
          isLargeScreen = true;
        } else {
          isLargeScreen = false;
        }
return isLargeScreen? _buildTabletLayout() : _buildMobileLayout();
複製代碼
  1. 若是隻是想給平板電腦設計的應用程序,那不能直接檢查MediaQurey的寬度來判斷,而是須要獲取Size並使用它來獲取實際的寬度。在橫屏的時候,width的值實際上是平板的長度,height的值實際上是平板的寬度。
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
}
複製代碼
  1. 強制橫豎屏的操做:須要進行強制橫屏或者豎屏,利用SystemChrome.setPreferredOrientations進行操做。
//強制豎屏
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown
  ]);
  //強制橫屏
   SystemChrome.setPreferredOrientations([
    DeviceOrientation.landscapeLeft,
    DeviceOrientation.landscapeRight
  ]);
複製代碼

Github連接

本身也跟着原做者擼了下demo,順便加了點註釋,方便本身理解。

本身的Github連接:github.com/LXD31256949…

原做者的Github鏈接:github.com/deven98/Flu…

相關文章
相關標籤/搜索