You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

237 lines
13 KiB
Markdown

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 33 | 如何适配不同分辨率的手机屏幕?
你好,我是陈航。
在上一篇文章中我与你分享了在Flutter中实现国际化的基本原理。与原生Android和iOS只需为国际化资源提供不同的目录就可以在运行时自动根据语言和地区进行适配不同Flutter的国际化是完全在代码中实现的。
即通过代码声明的方式将应用中所有需要翻译的文案都声明为LocalizationsDelegate的属性然后针对不同的语言和地区进行手动翻译适配最后在初始化应用程序时将这个代理设置为国际化的翻译回调。而为了简化这个过程也为了将国际化资源与代码实现分离我们通常会使用arb文件存储不同语言地区的映射关系并通过Flutter i18n插件来实现代码的自动生成。
可以说,国际化为全世界的用户提供了统一而标准的体验。那么,为不同尺寸、不同旋转方向的手机提供统一而标准的体验,就是屏幕适配需要解决的问题了。
在移动应用的世界中,页面是由控件组成的。如果我们支持的设备只有普通手机,可以确保同一个页面、同一个控件,在不同的手机屏幕上的显示效果是基本一致的。但,随着平板电脑和类平板电脑等超大屏手机越来越普及,很多原本只在普通手机上运行的应用也逐渐跑在了平板上。
由于平板电脑的屏幕非常大展示适配普通手机的界面和控件时可能会出现UI异常的情况。比如对于新闻类手机应用来说通常会有新闻列表和新闻详情两个页面如果我们把这两个页面原封不动地搬到平板电脑上就会出现控件被拉伸、文字过小过密、图片清晰度不够、屏幕空间被浪费的异常体验。
而另一方面即使对于同一台手机或平板电脑来说屏幕的宽高配置也不是一成不变的。因为加速度传感器的存在所以当我们旋转屏幕时屏幕宽高配置会发生逆转即垂直方向与水平方向的布局行为会互相交换从而导致控件被拉伸等UI异常问题。
因此,为了让用户在不同的屏幕宽高配置下获得最佳的体验,我们不仅需要对平板进行屏幕适配,充分利用额外可用的屏幕空间,也需要在屏幕方向改变时重新排列控件。即,我们需要优化应用程序的界面布局,为用户提供新功能、展示新内容,以将拉伸变形的界面和控件替换为更自然的布局,将单一的视图合并为复合视图。
在原生Android或iOS中这种在同一页面实现不同布局的行为我们通常会准备多个布局文件通过判断当前屏幕分辨率来决定应该使用哪套布局方式。在Flutter中屏幕适配的原理也非常类似只不过Flutter并没有布局文件的概念我们需要准备多个布局来实现。
那么今天,我们就来分别来看一下如何通过多个布局,实现适配屏幕旋转与平板电脑。
## 适配屏幕旋转
在屏幕方向改变时,屏幕宽高配置也会发生逆转:从竖屏模式变成横屏模式,原来的宽变成了高(垂直方向上的布局空间更短了),而高则变成了宽(水平方向上的布局空间更长了)。
通常情况下由于ScrollView和ListView的存在我们基本上不需要担心垂直方向上布局空间更短的问题大不了一屏少显示几个控件元素用户仍然可以使用与竖屏模式同样的交互滚动视图来查看其他控件元素但水平方向上布局空间更长界面和控件通常已被严重拉伸原有的布局方式和交互方式都需要做较大调整。
从横屏模式切回竖屏模式,也是这个道理。
为了适配竖屏模式与横屏模式我们需要准备两个布局方案一个用于纵向一个用于横向。当设备改变方向时Flutter会通知我们重建布局Flutter提供的OrientationBuilder控件可以在设备改变方向时通过builder函数回调告知其状态。这样我们就可以根据回调函数提供的orientation参数来识别当前设备究竟是处于横屏landscape还是竖屏portrait状态从而刷新界面。
如下所示的代码演示了OrientationBuilder的具体用法。我们在其builder回调函数中准确地识别出了设备方向并对横屏和竖屏两种模型加载了不同的布局方式而\_buildVerticalLayout和\_buildHorizontalLayout是用于创建相应布局的方法
```
@override
Widget build(BuildContext context) {
return Scaffold(
//使用OrientationBuilder的builder模式感知屏幕旋转
body: OrientationBuilder(
builder: (context, orientation) {
//根据屏幕旋转方向返回不同布局行为
return orientation == Orientation.portrait
? _buildVerticalLayout()
: _buildHorizontalLayout();
},
),
);
}
```
OrientationBuilder提供了orientation参数可以识别设备方向而如果我们在OrientationBuilder之外希望根据设备的旋转方向设置一些组件的初始化行为也可以使用MediaQueryData提供的orientation方法
```
if(MediaQuery.of(context).orientation == Orientation.portrait) {
//dosth
}
```
需要注意的是Flutter应用默认支持竖屏和横屏两种模式。如果我们的应用程序不需要提供横屏模式也可以直接调用SystemChrome提供的setPreferredOrientations方法告诉Flutter这样Flutter就可以固定视图的布局方向了
```
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
```
## 适配平板电脑
当适配更大的屏幕尺寸时我们希望App上的内容可以适应屏幕上额外的可用空间。如果我们在平板中使用与手机相同的布局就会浪费大量的可视空间。与适配屏幕旋转类似最直接的方法是为手机和平板电脑创建两种不同的布局。然而考虑到平板电脑和手机为用户提供的功能并无差别因此这种实现方式将会新增许多不必要的重复代码。
为解决这个问题,我们可以采用另外一种方法:**将屏幕空间划分为多个窗格即采用与原生Android、iOS类似的Fragment、ChildViewController概念来抽象独立区块的视觉功能。**
多窗格布局可以在平板电脑和横屏模式上实现更好的视觉平衡效果增强App的实用性和可读性。而我们也可以通过独立的区块在不同尺寸的手机屏幕上快速复用视觉功能。
如下图所示,分别展示了普通手机、横屏手机与平板电脑,如何使用多窗格布局来改造新闻列表和新闻详情交互:
![](https://static001.geekbang.org/resource/image/44/4f/44854c927812081c32d119886b12904f.png)
图1 多窗格布局示意图
首先,我们需要分别为新闻列表与新闻详情创建两个可重用的独立区块:
* 新闻列表可以在元素被点击时通过回调函数告诉父Widget元素索引
* 而新闻详情,则用于展示新闻列表中被点击的元素索引。
对于手机来说,由于空间小,所以新闻列表区块和新闻详情区块都是独立的页面,可以通过点击新闻元素进行新闻详情页面的切换;而对于平板电脑(和手机横屏布局)来说,由于空间足够大,所以我们把这两个区块放置在同一个页面,可以通过点击新闻元素去刷新同一页面的新闻详情。
页面的实现和区块的实现是互相独立的,通过区块复用就可以减少编写两个独立布局的工作:
```
//列表Widget
class ListWidget extends StatefulWidget {
final ItemSelectedCallback onItemSelected;
ListWidget(
this.onItemSelected,//列表被点击的回调函数
);
@override
_ListWidgetState createState() => _ListWidgetState();
}
class _ListWidgetState extends State<ListWidget> {
@override
Widget build(BuildContext context) {
//创建一个20项元素的列表
return ListView.builder(
itemCount: 20,
itemBuilder: (context, position) {
return ListTile(
title: Text(position.toString()),//标题为index
onTap:()=>widget.onItemSelected(position),//点击后回调函数
);
},
);
}
}
//详情Widget
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.red,//容器背景色
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(widget.data.toString()),//居中展示列表被点击元素索引
],
),
),
);
}
}
```
然后我们只需要检查设备屏幕是否有足够的宽度来同时展示列表与详情部分。为了获取屏幕宽度我们可以使用MediaQueryData提供的size方法。
在这里我们将平板电脑的判断条件设置为宽度大于480。这样屏幕中就有足够的空间可以切换到多窗格的复合布局了
```
if(MediaQuery.of(context).size.width > 480) {
//tablet
} else {
//phone
}
```
最后如果宽度够大我们就会使用Row控件将列表与详情包装在同一个页面中用户可以点击左侧的列表刷新右侧的详情如果宽度比较小那我们就只展示列表用户可以点击列表导航到新的页面展示详情
```
class _MasterDetailPageState extends State<MasterDetailPage> {
var selectedValue = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: OrientationBuilder(builder: (context, orientation) {
//平板或横屏手机页面内嵌列表ListWidget与详情DetailWidget
if (MediaQuery.of(context).size.width > 480) {
return Row(children: <Widget>[
Expanded(
child: ListWidget((value) {//在列表点击回调方法中刷新右侧详情页
setState(() {selectedValue = value;});
}),
),
Expanded(child: DetailWidget(selectedValue)),
]);
} else {//普通手机页面内嵌列表ListWidget
return ListWidget((value) {//在列表点击回调方法中打开详情页DetailWidget
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return Scaffold(
body: DetailWidget(value),
);
},
));
});
}
}),
);
}
}
```
运行一下代码,可以看到,我们的应用已经完全适配不同尺寸、不同方向的设备屏幕了。
![](https://static001.geekbang.org/resource/image/01/46/01edeb35d0780b197d7d61d43afa7546.gif)
图2 竖屏手机版列表详情
![](https://static001.geekbang.org/resource/image/a6/79/a60c85860e1dea73a2369b18482c8c79.gif)
图3 横屏手机版列表详情
![](https://static001.geekbang.org/resource/image/21/a8/217c8292073b5d0b13f86ea9c5b9e2a8.gif)
图4 竖屏平板列表详情
![](https://static001.geekbang.org/resource/image/96/62/96a2ab0fea7ffec12a507f6ced657d62.gif)
图5 横屏平板列表详情
## 总结
好了,今天的分享就到这里。我们总结一下今天的核心知识点吧。
在Flutter中为了适配不同设备屏幕我们需要提供不同的布局方式。而将独立的视觉区块进行封装通过OrientationBuilder提供的orientation回调参数以及MediaQueryData提供的屏幕尺寸以多窗格布局的方式为它们提供不同的页面呈现形态能够大大降低编写独立布局所带来的重复工作。如果你的应用不需要支持设备方向也可以通过SystemChrome提供的setPreferredOrientations方法强制竖屏。
做好应用开发,我们除了要保证产品功能正常,还需要兼容碎片化(包括设备碎片化、品牌碎片化、系统碎片化、屏幕碎片化等方面)可能带来的潜在问题,以确保良好的用户体验。
与其他维度碎片化可能带来功能缺失甚至Crash不同屏幕碎片化不至于导致功能完全不可用但控件显示尺寸却很容易在没有做好适配的情况下产生变形让用户看到异形甚至不全的UI信息影响产品形象因此也需要重点关注。
在应用开发中我们可以分别在不同屏幕尺寸的主流机型和模拟器上运行我们的程序来观察UI样式和功能是否异常从而写出更加健壮的布局代码。
我把今天分享所涉及到的知识点打包到了[GitHub](https://github.com/cyndibaby905/33_multi_screen_demo)中,你可以下载下来,反复运行几次,加深理解与记忆。
## 思考题
最后,我给你留下一道思考题吧
setPreferredOrientations方法是全局生效的如果你的应用程序中有两个相邻的页面页面A仅支持竖屏页面B同时支持竖屏和横屏你会如何实现呢
欢迎你在评论区给我留言分享你的观点,我会在下一篇文章中等待你!感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。