Flutter 中的针对多屏幕尺寸和方向进行开发

798 阅读9分钟

移动应用程序需要支持各种设备尺寸、像素密度和方向。应用程序需要能够很好地拓展、处理方向变化并通过所有这些来持久保存数据。Flutter使您能够选择应对这些挑战的方式,而不是只提供一种特定的解决方案。

一、用于处理大屏幕的Android的解决方案

在Android中,我们使用备用布局文件来处理平板电脑等较大的屏幕,我们可以为最小宽度和横向/纵向方向定义这些文件。

image.png 这意味着我们必须为手机定义一个布局文件,为平板电脑定义一个布局文件,然后为每种设备类型定义两个方向。然后根据运行它的任何设备实例化这些布局。然后我们检查哪个布局处于活动状态(手机/平板电脑)并进行相应的初始化。

对于大多数应用程序,master-detail flow用于处理较大的屏幕尺寸,它使用Fragments。稍后我们将讨论什么是master-detail flow

Android中的Fragment本质上是可以在屏幕中使用的可重用组件,Fragment有自己的布局并且Java/Kotlin类来控制数据和Fragment的生命周期。这是一项相当大的工作,需要大量代码才能开始工作。

让我们先看看处理方向,然后再处理Flutter的屏幕尺寸。

二、在Flutter中处理方向

当我们使用方向时,我们希望使用屏幕的整个宽度来显示尽可能多的信息。

下面的示例在两个方向上创建了一个基本的个人资料页面,并根据方向构建不同的布局,以最大限度地利用屏幕宽度。

1_rbXi0HAzmRIYUnXXO74MRQ.gif

这里我们有一个简单的屏幕,它有不同的纵向和横向布局。让我们尝试通过创建上面的示例来了解我们实际上是如何在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()_buildHorizontalLayout()是我们编写的用于创建相应布局的方法 。

我们还可以在代码的任何一点(在OrientationBuilder内部或外部)使用。

MediaQuery.of(context).orientation

注意:在我们只想使用竖屏的时候,使用

SystemChrome.setPreferredOrientations(DeviceOrientation.portraitUp);

四、在Flutter中为大屏幕创建布局

当我们处理更大的屏幕尺寸时,我们希望我们的屏幕适应使用屏幕上的可用空间。最直接的方法就是为平板电脑和手机创建两种不同的布局甚至屏幕。(这里,“布局”是指屏幕的可见部分。“屏幕”是指布局以及与之相关的所有后端代码。)然而,这涉及到很多不必要的代码,并且需要重复代码。

那么我们要怎么做才能解决这个问标题呢?

首先,让我们看一下最常见的用例。

让我们回到我们要讨论的Master-Detail Flow。对于应用程序,您会看到一个常见的模式,即您有一个项目的助理表,当您单击一个列表项时,您会被重定向到一个不同的详细信息屏幕。以Gmail为例,我们有一个电子邮件列表,当我们点击其中一个时,会打开一个包含邮件内容的详细视图。

image.png

让我们为此流程制作一个示例应用程序。

1_nh0egImdXzJGjZZ1TeAXNw.gif 这个应用程序只包含一个数字列表,并在点击时突出显示一个数字。我们有一个数字的主列表和一个单击时显示数字的详细视图。就像电子邮件一样。

如果我们在平板电脑中使用相同的布局,那将是相当大的空间浪费。那么我们可以做些什么来解决呢?由于我们有可用的屏幕空间,我们可以在同一屏幕上同时显示主列表和详细信息视图。

1_lOzCYX8pLD-yJAJ0UwaVhA.gif 那么我们可以做些什么来减少编写两个单独屏幕的工作呢?

让我们看看Android是如何解决这个问题的。Android从主列表和详细信息视图中创建成为Fragment的可复用组件。Fragment可以与屏幕分开定义,只需将其添加到屏幕中即可,而无需重复代码两次。

image.png

所以Fragment A是主列表fragment而Fragment B是详情fragment。在手机或更小宽度的布局中,单击列表项将导航到单独的页面,而在平板电脑中它将保留在同一页面上并更改详细信息Fragment。当手机旋转到横向时,我们还可以做一个类似平板电脑的界面。

这就是Flutter的强大之处。

Flutter中的每个小部件本质上都是可重用的。

Flutter中的每个小部件都像一个Fragment。

我们需要做的就是定义两个小部件。一个用于列表,一个用于详细视图。这些实际上是fragmnet。我们只是检查设备是否有足够的宽度来处理列表和详细信息部分。如果是这样,我们将同时使用这两个小部件。如果设备没有足够的宽度来支持两者,我们只显示列表并导航到单独的屏幕以显示详细内容。

我们首先需要检查设备的宽度,看看我们是否可以使用较大的布局而不是较小的布局。为了获得宽度,我们使用

MediaQuery.of(context).size.width

Size以dps为单位为我们提供了设备的高度和宽度。

让我们将最小宽度设置为600dp以切换到第二个布局。

加起来

  1. 我们创建了两个小部件,一个包含主列表,一个包含详细视图。
  2. 我们创建了两个屏幕。在第一个屏幕上,我们检查设备是否有足够的宽度来处理两个小部件。
  3. 如果有足够的宽度,我们会在一页上添加两个小部件。如果没有,我们会在点击只有详细视图的列表项时候导航到第二页。

五、让我们编码吧

让我们对我在本节顶部包含的演示进行编码、其中我们有一个数字列表,详细视图显示该数字。首先我们只做两个小部件。

列表小部件

image.png

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(
                    paddind: 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围绕它以相应点击。

详细信息小部件

image.png

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

Detail Widget只需要一个数字并将其显示在显著位置。

请注意,这些不是屏幕。这些只是我们要在屏幕上使用的小部件。

主屏幕

image.png

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来包裹小部件以填充屏幕或在屏幕较大的情况下将屏幕分成一定比例。所以Expanded通过设置Flex属性可以让每个widget占满屏的一半甚至一定比例。

第二个重要部分是:

if (isLargeScreen) {
    selectedValue = value;
    setState(() {});
} else {
    Navigator.push(context, MaterialPageRoute(
        builder: (context) {
            return DetailPage(value);
        }
    ));
}

这意味着,如果使用更大的布局,我们不需要转到不同的屏幕,因为详细信息小部件在页面本身上。如果屏幕较小,我们需要导航到不同的页面,因为当前屏幕只显示列表。

最后,详细信息页面(适用于较小的屏幕)

image.png

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: DetailWiget(widget.data),
        );
    }
}

它在页面上只包含一个详细信息小部件,用于在较小的屏幕上显示数据。

现在我们有一个可以适应不同尺寸和方向的屏幕的功能应用程序。 image.png

六、一些更重要的事情

  1. 如果你只想有不同布局,而不是任何类似片段的布局,你可以简单地在构建方法中编写
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
}

原文翻译博客:Developing for Multiple Screen Sizes and Orientations in Flutter (Fragments in Flutter)