Flutter之支援不同的螢幕尺寸和方向

入魔的冬瓜發表於2019-01-21

介紹

本文是medium的一篇文章的翻譯,再加上自己的一點理解,已得到作者的同意。

主要講的是在平板和手機中,處理適配不同螢幕的問題。

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

Android中解決大螢幕的方法

在Android中,我們處理比較大尺寸的螢幕,例如平板電腦。我們可以用過最小寬度限定符來定義相應尺寸的佈局檔案的名稱。

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的生命週期。這是一項相當大的工作,需要大量程式碼才能開始工作。

下面,我們先來看看在Flutter中處理螢幕方向,然後處理Flutter的螢幕尺寸。

在Flutter中使用方向

當我們使用螢幕方向的時候,我們希望使用螢幕的全部寬度和顯示儘可能的最大資訊量。

下面的示例,在兩個方向上建立一個基本的配置檔案頁面,並根據不同的方向去構建佈局,以最大限度地使用螢幕寬度。

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可以在裝置的方向發生改變的時候,重新構建佈局。

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…

相關文章