在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()和_buildHorizontalLayout()是我编写的用于创建相应布局的方法。
我们还可以使用代码检查代码中的任何位置(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,以切换到第二种布局。
总结:
- 我们创建了两个小部件,一个包含主列表,另一个包含详细视图。
- 我们创建两个屏幕。在第一个屏幕上,我们检查设备是否有足够的宽度来处理这两个小部件。
- 如果有足够的宽度,我们在一个页面上添加两个小部件。如果没有,我们在点击列表项时导航到第二页,该列表项只有详细视图。
我们来编码吧
让我们编写我在本节顶部包含的演示代码,其中我们有一个数字列表,详细信息视图显示该数字。首先我们制作两个小部件。
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 {
@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: [
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 {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.blue,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(widget.data.toString(), style: TextStyle(fontSize: 36.0, color: Colors.white),),
],
),
),
);
}
}
细节小部件只需一个数字并显着地显示它。
请注意,这些不是屏幕。这些只是我们将在屏幕上使用的小部件。
主屏幕
class MasterDetailPage extends StatefulWidget {
@override
_MasterDetailPageState createState() => _MasterDetailPageState();
}
class _MasterDetailPageState extends State {
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: [
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 {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: DetailWidget(widget.data),
);
}
}
它只在页面上保存一个细节小部件,用于在较小的屏幕上显示数据。
现在我们有一个功能正常的应用程序,适应不同大小和方向的屏幕。
一些更重要的事情
- 如果你想简单地拥有不同的布局而没有任何类似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