Flutter 是一个跨平台的应用程序开发框架,支持屏幕尺寸变化很大的设备:它可以在小到智能手表的设备上运行,也可以运行在大电视等设备上。使用相同的代码库使您的应用程序适应如此多样的屏幕尺寸和像素密度始终是一个挑战。在 Flutter 中设计响应式布局没有硬性规定。在本文中,我将向您展示一些在设计此类布局时可以遵循的方法。在继续在 Flutter 中构建响应式布局之前,我想说明一下
Android和iOS如何处理不同屏幕尺寸的原生布局。那么,让我们开始吧,但首先,让我们知道
您在 Git 存储库中有多少移动应用程序项目?
安卓方法
为了处理不同的屏幕尺寸和像素密度,Android 中使用了以下概念:
1. 约束布局
在 Android 世界中引入的用于 UI 设计的革命性工具之一是?ConstraintLayout。它可用于创建适应不同屏幕尺寸和尺寸的灵活且响应迅速的 UI 设计。ConstraintLayout 允许您根据与布局中其他视图的空间关系为每个视图指定位置和大小。
但这并不能解决大型设备的问题,在这种情况下,仅仅拉伸或调整 UI 组件的大小并不是利用屏幕空间的最优雅方式。这也适用于像智能手表这样的设备,它们的屏幕空间很小,调整组件大小以适应屏幕大小可能会导致奇怪的 UI。
2. 替代布局
为了解决上述问题,您可以为不同尺寸的设备使用替代布局。例如,您可以在平板电脑等设备中使用拆分视图来提供良好的用户体验并明智地使用大屏幕空间。![
在 Android 中,您可以为不同的屏幕尺寸定义
单独的布局文件,Android 框架会根据设备的屏幕尺寸自动处理这些布局之间的切换。![
?随时了解应用开发新闻
3. 片段
使用?Fragment,您可以将 UI 逻辑提取到单独的组件中,以便在为大屏幕尺寸设计多窗格布局时,您不必单独定义逻辑。您可以重用您为每个片段定义的逻辑。
4.矢量图形
与使用像素位图创建相反,矢量图形是在 XML 文件中定义为路径和颜色的图像。它们可以缩放到任何大小而不会缩放工件。在 Android 中,您可以将?VectorDrawable用于任何类型的插图,例如图标。
iOS方法
iOS 用于定义响应式布局的概念如下:
1. 自动布局
?自动布局可用于构建自适应界面,您可以在其中定义管理应用程序内容的规则(称为约束)。当检测到某些环境变化(称为特征)时,自动布局会根据指定的约束自动重新调整布局。
2. 尺码等级
大小类是根据大小自动分配给内容区域的特征。iOS 根据内容区域的大小类别动态调整布局。在 iPad 上,当你的 app 在?多任务配置中运行时,size classes 也适用。
3.一些UI元素
还有一些其他 UI 元素可用于在 iOS 上构建响应式 UI,例如?UIStackView、?UIViewController和[?UISplitViewController。
Flutter 有何不同
即使您不是 Android 或 iOS 开发人员,此时您也应该已经了解这些平台如何处理本机响应。在 Android 中,要在单个屏幕上显示多个 UI 视图,您可以使用 Fragments,它们就像可以在应用程序的 Activity 内运行的可重用组件。
您可以在一个 Activity 中运行多个 Fragment,但不能同时在单个应用程序中运行多个 Activity。
在 iOS 中,UISplitViewController
以分层界面管理子视图控制器,用于控制多个视图控制器。现在,让我们继续讨论
Flutter。Flutter 引入了[
?widgets的概念。基本上,它们是可以连接在一起以构建整个应用程序的构建块。
请记住,在 Flutter 中,每个屏幕甚至整个应用程序也是小部件!
小部件本质上是可重用的,因此您在 Flutter 中构建响应式布局时无需学习任何其他概念。
Flutter 中的响应能力
正如我之前所说,我将介绍开发响应式布局所需的重要概念,然后,您可以选择如何在应用程序中实现它们。
1. 媒体查询
您可以使用?MediaQuery来检索?屏幕的大小(宽度/高度)和方向(纵向/横向)。一个例子如下:
代码语言:javascript复制
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
Size screenSize = MediaQuery.of(context).size;
Orientation orientation = MediaQuery.of(context).orientation;
return Scaffold(
body: Container(
color: CustomColors.android,
child: Center(
child: Text(
'Viewnn'
'[MediaQuery width]: ${screenSize.width.toStringAsFixed(2)}nn'
'[MediaQuery orientation]: $orientation',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
);
}
}
2. 布局构建器
使用[?LayoutBuilder类,您可以获得[?BoxConstraints对象,该对象可用于确定小部件的maxWidth和maxHeight。
记住:之间的主要区别
MediaQuery
和LayoutBuilder
是MediaQuery使用屏幕的完整范围内,而不是你的特定图标的只是大小,而LayoutBuilder能够确定特定部件的最大宽度和高度。
证明这一点的示例如下:
代码语言:javascript复制
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
Size screenSize = MediaQuery.of(context).size;
return Scaffold(
body: Row(
children: [
Expanded(
flex: 2,
child: LayoutBuilder(
builder: (context, constraints) => Container(
color: CustomColors.android,
child: Center(
child: Text(
'View 1nn'
'[MediaQuery]:n ${screenSize.width.toStringAsFixed(2)}nn'
'[LayoutBuilder]:n${constraints.maxWidth.toStringAsFixed(2)}',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
),
),
Expanded(
flex: 3,
child: LayoutBuilder(
builder: (context, constraints) => Container(
color: Colors.white,
child: Center(
child: Text(
'View 2nn'
'[MediaQuery]:n ${screenSize.width.toStringAsFixed(2)}nn'
'[LayoutBuilder]:n${constraints.maxWidth.toStringAsFixed(2)}',
style: TextStyle(color: CustomColors.android, fontSize: 18),
),
),
),
),
),
],
),
);
}
}
3. 当前方向
要确定小部件的当前方向,您可以使用[?OrientationBuilder类。
**记住:**这与您可以使用 检索的设备方向不同
MediaQuery
。
证明这一点的示例如下:
代码语言:javascript复制
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
Orientation deviceOrientation = MediaQuery.of(context).orientation;
return Scaffold(
body: Column(
children: [
Expanded(
flex: 2,
child: Container(
color: CustomColors.android,
child: OrientationBuilder(
builder: (context, orientation) => Center(
child: Text(
'View 1nn'
'[MediaQuery orientation]:n$deviceOrientationnn'
'[OrientationBuilder]:n$orientation',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
),
),
Expanded(
flex: 3,
child: OrientationBuilder(
builder: (context, orientation) => Container(
color: Colors.white,
child: Center(
child: Text(
'View 2nn'
'[MediaQuery orientation]:n$deviceOrientationnn'
'[OrientationBuilder]:n$orientation',
style: TextStyle(color: CustomColors.android, fontSize: 18),
),
),
),
),
),
],
),
);
}
}
4. 扩展性和灵活性
在 aColumn
或 aRow
中特别有用的小部件是Expanded
and Flexible
。该?扩展插件扩展行,列的孩子,或Flex使孩子充满可用空间,而?灵活的不一定填满整个可用空间。一个例子示给出的各种组合
Expanded
和Flexible
是如下:
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
children: [
Row(
children: [
ExpandedWidget(),
FlexibleWidget(),
],
),
Row(
children: [
ExpandedWidget(),
ExpandedWidget(),
],
),
Row(
children: [
FlexibleWidget(),
FlexibleWidget(),
],
),
Row(
children: [
FlexibleWidget(),
ExpandedWidget(),
],
),
],
),
),
);
}
}
class ExpandedWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Expanded(
child: Container(
decoration: BoxDecoration(
color: CustomColors.android,
border: Border.all(color: Colors.white),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Expanded',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
),
);
}
}
class FlexibleWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Flexible(
child: Container(
decoration: BoxDecoration(
color: CustomColors.androidAccent,
border: Border.all(color: Colors.white),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Flexible',
style: TextStyle(color: CustomColors.android, fontSize: 24),
),
),
),
);
}
}
5. FractionallySizedBox
该?FractionallySizedBox部件有助于大小及其子总的可用空间的一小部分。它在内部Expanded
或Flexible
小部件中特别有用。一个例子如下:
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FractionallySizedWidget(widthFactor: 0.4),
],
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FractionallySizedWidget(widthFactor: 0.6),
],
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FractionallySizedWidget(widthFactor: 0.8),
],
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FractionallySizedWidget(widthFactor: 1.0),
],
),
],
),
),
);
}
}
class FractionallySizedWidget extends StatelessWidget {
final double widthFactor;
FractionallySizedWidget({@required this.widthFactor});
@override
Widget build(BuildContext context) {
return Expanded(
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: widthFactor,
child: Container(
decoration: BoxDecoration(
color: CustomColors.android,
border: Border.all(color: Colors.white),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'${widthFactor * 100}%',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
),
),
);
}
}
6. 纵横比
您可以使用?AspectRatio小部件将子项调整为特定的纵横比。这个小部件首先尝试布局约束允许的最大宽度,然后通过将给定的纵横比应用于宽度来决定高度。
代码语言:javascript复制dart
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
children: [
AspectRatioWidget(ratio: '16 / 9'),
AspectRatioWidget(ratio: '3 / 2'),
],
),
),
);
}
}
class AspectRatioWidget extends StatelessWidget {
final String ratio;
AspectRatioWidget({@required this.ratio});
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: Fraction.fromString(ratio).toDouble(),
child: Container(
decoration: BoxDecoration(
color: CustomColors.android,
border: Border.all(color: Colors.white),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Text(
'AspectRatio - $ratio',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
),
),
);
}
}
我们已经研究了在 Flutter 中构建响应式布局所需的大部分重要概念,除了一个。 让我们在构建示例响应式应用程序时学习最后一个概念。
构建响应式应用程序
现在,我们将应用我在上一节中描述的一些概念。除此之外,您还将学习构建大屏幕布局的另一个重要概念:拆分视图。我们将构建一个名为
Flow的示例聊天应用程序设计。
该应用程序将主要由两个主要屏幕组成:
- 主页(
PeopleView
,BookmarkView
,ContactView
) - 聊天页面(
PeopleView
,ChatView
)
主页
启动后应用程序的主屏幕将是HomePage
. 它由两种类型的视图组成:
- HomeViewSmall(包括
AppBar
,Drawer
,BottomNavigationBar
,和DestinationView
) - HomeViewLarge(由分割视图、
MenuWidget
、 和 组成DestinationView
)
dart
class _HomePageState extends State {
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 600) {
return HomeViewSmall();
} else {
return HomeViewLarge();
}
},
),
);
}
}
在这里,LayoutBuilder
是用于确定maxWidth
与之间的切换HomeViewSmall
和HomeViewLarge
窗口小部件。
dart
class _HomeViewSmallState extends State {
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// ...
),
drawer: Drawer(
// ...
),
bottomNavigationBar: BottomNavigationBar(
// ...
),
body: IndexedStack(
index: _currentIndex,
children: allDestinations.map((Destination destination) {
return DestinationView(destination);
}).toList(),
),
);
}
}
IndexedStack
withDestinationView
用于根据BottomNavigationBar
.
代码语言:javascript复制如果您想了解更多信息,请查看本文末尾提供的此示例应用程序的GitHub存储库。
dart
class _HomeViewLargeState extends State {
int _index = 0;
@override
Widget build(BuildContext context) {
return Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: MenuWidget(
selectedIndex: _index,
onTapped: (selectedIndex) {
setState(() {
_index = selectedIndex;
});
},
),
),
Expanded(
flex: 3,
child: IndexedStack(
index: _index,
children: allDestinations.map((Destination destination) {
return DestinationView(destination);
}).toList(),
),
),
],
),
);
}
}
对于大屏幕,我们将显示包含MenuWidget
和 的拆分视图DestinationView
。可以看到,在 Flutter 中创建拆分视图真的很容易。您只需使用 a 将它们并排放置Row
,然后,为了填满整个空间,只需使用Expanded
小部件包装两个视图。您还可以定义小部件的flex
属性Expanded
,这将让您指定每个小部件应覆盖多少屏幕(默认情况下,flex
设置为1)。!
但是现在,如果您移动到特定屏幕然后在视图之间切换,您将丢失页面的上下文;也就是说,您将始终返回第一页,即
Chats。为了解决这个问题,我使用了多个回调函数将所选页面返回到HomePage
. 实际上,您应该使用状态管理技术来处理这种情况。由于本文的唯一目的是教您构建响应式布局,因此我不会涉及状态管理的任何复杂性。修改
HomeViewSmall
:
dart
class HomeViewSmall extends StatefulWidget {
final int currentIndex;
/// Callback function
final Function(int selectedIndex) onTapped;
HomeViewSmall(this.currentIndex, this.onTapped);
@override
_HomeViewSmallState createState() => _HomeViewSmallState();
}
class _HomeViewSmallState extends State {
int _currentIndex = 0;
@override
void initState() {
super.initState();
_currentIndex = widget.currentIndex;
}
@override
Widget build(BuildContext context) {
return Scaffold(
// ...
bottomNavigationBar: BottomNavigationBar(
// ...
currentIndex: _currentIndex,
onTap: (int index) {
setState(() {
_currentIndex = index;
// Invoking the callback
widget.onTapped(_currentIndex);
});
},
items: allDestinations.map((Destination destination) {
return BottomNavigationBarItem(
icon: Icon(destination.icon),
label: destination.title,
);
}).toList(),
),
);
}
}
修改HomeViewLarge
:
dart
class HomeViewLarge extends StatefulWidget {
final int currentIndex;
/// Callback function
final Function(int selectedIndex) onTapped;
HomeViewLarge(this.currentIndex, this.onTapped);
@override
_HomeViewLargeState createState() => _HomeViewLargeState();
}
class _HomeViewLargeState extends State {
int _index = 0;
@override
void initState() {
super.initState();
_index = widget.currentIndex;
}
@override
Widget build(BuildContext context) {
return Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: MenuWidget(
selectedIndex: _index,
onTapped: (selectedIndex) {
setState(() {
_index = selectedIndex;
// Invoking the callback
widget.onTapped(_index);
});
},
),
),
// ...
],
),
);
}
}
```dart
修改`HomePage`:
dart
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State {
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
代码语言:javascript复制return Scaffold(
body: LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 600) {
return HomeViewSmall(_currentIndex, (index) {
setState(() {
_currentIndex = index;
});
});
} else {
return HomeViewLarge(_currentIndex, (index) {
setState(() {
_currentIndex = index;
});
});
}
},
),
);
}
}
代码语言:javascript复制
现在,您的完全响应`HomePage`已完成。
### 聊天页面
这将类似于`HomePage`,但它将包含以下两个视图:
- **ChatViewSmall**(包括`AppBar`,`ChatList`,和`SendWidget`插件)
- **ChatViewLarge**(包括`PeopleView`,`ChatList`,和`SendWidget`插件)
```dart
dart
class ChatPage extends StatelessWidget {
final Color profileIconColor;
ChatPage(this.profileIconColor);
@override
Widget build(BuildContext context) {
return Scaffold(
body: OrientationBuilder(
builder: (context, orientation) => LayoutBuilder(
builder: (context, constraints) {
double breakpointWidth = orientation == Orientation.portrait ? 600 : 800;
if (constraints.maxWidth < breakpointWidth) {
return ChatViewSmall(profileIconColor);
} else {
return ChatViewLarge(profileIconColor);
}
},
),
),
);
}
}
在这里,我使用OrientationBuilder
了LayoutBuilder
来breakpointWidth
根据方向改变 ,因为我不想PeopleView
在处于横向模式时在小屏幕手机上显示。
dart
class ChatViewSmall extends StatelessWidget {
final Color profileIconColor;
ChatViewSmall(this.profileIconColor);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
),
body: Container(
color: Colors.white,
child: Column(
children: [
Expanded(child: ChatList(profileIconColor)),
SendWidget(),
],
),
),
);
}
}
代码语言:javascript复制dart
class ChatViewLarge extends StatelessWidget {
final Color profileIconColor;
ChatViewLarge(this.profileIconColor);
@override
Widget build(BuildContext context) {
return Container(
child: Row(
children: [
Expanded(
flex: 2,
child: SingleChildScrollView(
child: PeopleView(),
),
),
Expanded(
flex: 3,
child: Container(
color: Colors.white,
child: Column(
children: [
Expanded(child: ChatList(profileIconColor)),
SendWidget(),
],
),
),
),
],
),
);
}
}
结论
我们已经成功地在 Flutter 中创建了一个完全响应的应用程序。您仍然可以对此应用程序进行许多改进,其中之一可能是根据不同的屏幕尺寸定义不同的fontSize。在使用响应能力时,您可以使用的一些令人惊叹的 Flutter 插件如下:
- ?device_preview
- ?device_preview
- ?responsive_builde
- ?https://pub.dev/packages/responsive_framework