Flutter 是一个跨平台的利用开发框架,反对各种屏幕大小的设施,它能够在智能手表这样的小设施上运行,也能够在电视这样的大设施上运行。应用雷同的代码来适应不同的屏幕大小和像素密度是一个挑战。
Flutter 响应式布局的设计没有硬性的规定。在本文中,我将向您展现在设计响应式布局时能够遵循的一些办法。
在应用 Flutter 构建响应式布局之前,我想阐明一下 Android 和 iOS 是如何解决不同屏幕大小的布局的。
1. Android 的办法
为了解决不同的屏幕尺寸和像素密度,在 Android 中应用了以下概念:
1.1 ConstraintLayout
Android UI 设计中引入的一个革命性的货色是 ConstraintLayout。它能够用于创立灵便的、响应性强的 UI 设计,以适应不同的屏幕大小和尺寸。它容许您依据与布局中其余视图的空间关系来指定每个视图的地位和大小。
但这并不能解决大型设施的问题,在大型设施中,拉伸或只是调整 UI 组件的大小并不是利用屏幕面积的最优雅的形式。在屏幕面积很小的智能手表,调整组件以适应屏幕大小可能会导致奇怪的 UI。
1.2 Alternative layouts
要解决上述问题,您能够为不同大小的设施应用 alternative layouts。例如,你能够在平板电脑等设施上应用分屏视图来提供良好的用户体验,并明智地应用大屏幕。
在 Android 中,你能够为不同的屏幕大小定义不同的布局文件,Android 框架会依据设施的屏幕大小主动解决这些布局之间的切换。
1.3 Fragments
应用 Fragment,你能够将你的 UI 逻辑提取到独自的组件中,这样当你为大屏幕尺寸设计多窗格布局时,你不用独自定义逻辑。您能够重用为每个片段定义的 Fragment。
1.4 Vector graphics
Vector graphics 应用 XML 创立图像来定义门路和色彩,而不是应用像素位图。它能够缩放到任何大小。在 Android 中,你能够应用 VectorDrawable 来绘制任何类型的插图,比方图标。
2. iOS 的办法
iOS 用于定义响应式布局的形式如下
2.1 Auto Layout
Auto Layout 可用于构建自适应界面,您能够在其中定义用于控制应用程序内容的规定(称为束缚)。当检测到某些环境变动(称为特色)时,“Auto Layout”会依据指定的约束条件主动从新调整布局。
2.2 Size classes
Size 类的特点是会依据其大小主动调配给内容区域。iOS 会依据内容区域的 Size 类别动静地进行布局调整。在 iPad 上,size 类也实用。
2.3 一些 UI 组件
还有一些其余的 UI 嘴贱你能够用来在 iOS 上构建响应式 UI,像 UIStackView, UIViewController,和 UISplitViewController。
3. Flutter 是如何自适应的
即便你不是 Android 或 iOS 的开发者,到目前为止,你应该曾经理解了这些平台是如何解决响应式布局的。
在 Android 中,要在单个屏幕上显示多个 UI 视图,请应用 Fragments,它们相似于可在应用程序的 Activity 中运行的可重用组件。
您能够在一个 Activity 中运行多个 Fragment,然而不能在一个应用程序中同时运行多个 Activity。
在 iOS 中,为了管制多个视图控制器,应用了 UISplitViewController,它在分层界面中治理子视图控制器。
当初咱们来到 Flutter
Flutter 引入了 widget 的概念。它们像积木一样拼凑在一起构建应用程序画面。
记住,在 Flutter 中,每个屏幕和整个应用程序也是一个 widget!
widget 实质上是可重用的,因而在 Flutter 中构建响应式布局时,您不须要学习任何其余概念。
3.1 Flutter 的响应式概念
正如我后面所说的,我将探讨开发响应式布局所需的重要概念,而后你来抉择应用什么样的形式在你的 APP 上实现响应式布局。
3.1.1 MediaQuery
你能够应用 MediaQuery 来检索屏幕的大小 (宽度 / 高度) 和方向(纵向 / 横向)。
上面是例子
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(
'View\n\n' +
'[MediaQuery width]: ${screenSize.width.toStringAsFixed(2)}\n\n' +
'[MediaQuery orientation]: $orientation',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
);
}
}
3.1.2 LayoutBuilder
应用 LayoutBuilder 类,您能够取得 BoxConstraints 对象,该对象可用于确定小部件的 maxWidth 和 maxHeight。
请记住:MediaQuery 和 LayoutBuilder 之间的次要区别在于,MediaQuery 应用屏幕的残缺上下文,而不仅仅是特定小部件的大小。而 LayoutBuilder 能够确定特定小部件的最大宽度和高度。
上面是例子
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 1\n\n' +
'[MediaQuery]:\n ${screenSize.width.toStringAsFixed(2)}\n\n' +
'[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 2\n\n' +
'[MediaQuery]:\n ${screenSize.width.toStringAsFixed(2)}\n\n' +
'[LayoutBuilder]:\n${constraints.maxWidth.toStringAsFixed(2)}',
style: TextStyle(color: CustomColors.android, fontSize: 18),
),
),
),
),
),
],
),
);
}
}
PS: 当你在构建一个小部件,想晓得他的宽度是多少时,应用这个组件,你能够依据子组件可用高 / 宽度来进行判断,构建不同的布局
3.1.3 OrientationBuilder
要确定 widget 的以后方向,能够应用 OrientationBuilder 类。
记住: 这与你应用 MediaQuery 检索的设施方向不同。
上面是例子
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 1\n\n' +
'[MediaQuery orientation]:\n$deviceOrientation\n\n' +
'[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 2\n\n' +
'[MediaQuery orientation]:\n$deviceOrientation\n\n' +
'[OrientationBuilder]:\n$orientation',
style: TextStyle(color: CustomColors.android, fontSize: 18),
),
),
),
),
),
],
),
);
}
}
portrait (纵向) landscape(横向)
PS:看了下 OrientationBuilder 的源码正文
widget 的方向仅仅是其宽度绝对于高度的一个系数。如果一个 [Column] 部件的宽度超过了它的高度,它的方向是横向的,即便它以垂直的模式显示其子元素。
这是译者的代码
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
/// Copyright (C), 2020-2020, flutter_demo
/// FileName: orientationBuilder_demo
/// Author: Jack
/// Date: 2020/12/6
/// Description:
class OrientationBuilderDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {Orientation deviceOrientation = MediaQuery.of(context).orientation;
return Scaffold(
body: Column(
children: [
Expanded(
flex: 1,
child: Container(
color: Colors.greenAccent,
child: OrientationBuilder(builder: (context, orientation) => Center(
child: Text(
'View 1\n\n' +
'[MediaQuery orientation]:\n$deviceOrientation\n\n' +
'[OrientationBuilder]:\n$orientation',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
),
),
Expanded(
flex: 2,
child: OrientationBuilder(builder: (context, orientation) => Container(
color: Colors.white,
child: Center(
child: Text(
'View 2\n\n' +
'[MediaQuery orientation]:\n$deviceOrientation\n\n' +
'[OrientationBuilder]:\n$orientation',
style: TextStyle(color: Colors.greenAccent, fontSize: 18),
),
),
),
),
),
],
),
);
}
}
想必你曾经了解了 OrientationBuilder 的方向定义,如果一个小部件的严惩于高,他就是横向的,如果高大于宽,他就是横向的,仅此而已。
3.1.4 Expanded and Flexible
在 Row 或 Column 中特地有用的小部件是 Expanded 和 Flexible。当 Expanded 应用在一个 Row、Column 或 Flex 中,Expanded 能够使它的子 Widget 主动填充可用空间,与之相同,Flexible 的子 widget 不会填满整个可用空间。
例子如下。
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),
),
),
),
);
}
}
PS: 与 [expand] 不同的是,[Flexible]不须要子 widget 填充残余的空间,第一个例子,expanded 尽管有填充空余空间的性能,不过 expanded 组件和 flexible 组件的 flex 都是 1, 相当于将纵轴分成两半,expanded 所领有的全副空间就是纵轴的一半,理论他曾经填充了。
3.1.5 FractionallySizedBox
FractionallySizedBox widget 将其子元素的大小调整为可用空间的一小部分。它在 Expanded 或 Flexible widget 中特地有用。
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),
),
),
),
),
);
}
}
PS: 当你想让你的 widget,占据以后屏幕宽度和高度的百分之多少时,应用这个组件,想在 Row 和 Column 组件中应用百分比布局时,须要在 FractionallySizedBox 外包裹一个 expanded 或 flexible
3.1.6 AspectRatio
能够应用 AspectRatio 小部件将子元素的大小调整为特定的长宽比。首先,它尝试布局束缚容许的最大宽度,并通过将给定的高宽比利用于宽度来决定高度。
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 app,除了最初一个。
在构建一个示例响应式应用程序时,让咱们学习最初一个概念。
3.2 创立一个响应式 APP
当初,咱们将利用上一节中形容的一些概念。与此同时,您还将学习为大屏幕构建布局的另一个重要概念,即分屏视图(一个屏幕上显示多个页面)。
响应式布局:在不同大小的屏幕上应用不同的布局。
咱们将建设一个名叫 Flow 的聊天应用程序。
app 次要由两个局部组成:
- HomePage (
PeopleView
,BookmarkView
,ContactView
) -
ChatPage (
PeopleView
,ChatView
)
对于大屏幕,咱们将显示蕴含 MenuWidget 和 DestinationView 的分屏视图。您能够看到,在 Flutter 中创立分屏视图是非常容易的,您只需应用一行将它们并排搁置,而后为了填满整个空间,只需应用 Expanded widget 包装两个视图。您还能够定义扩大小部件的 flex 属性,这将容许您指定每个小部件应该笼罩屏幕的多少局部(默认 flex 设置为 1)。
然而,如果您当初挪动到一个特定的屏幕,而后在视图之间切换,那么您将失落页面的上下文,也就是说您将始终返回到第一个页面,即“聊天”。为了解决这个问题,我应用了多个回调函数来返回所选页面到主页。实际上,您应该应用状态治理技术来解决此场景。因为本文的惟一目标是教您构建响应式布局,所以我不探讨任何状态治理的复杂性。