解密Flutter响应式布局

Flutter是一个跨平台的利用开发框架,反对各种屏幕大小的设施,它能够在智能手表这样的小设施上运行,也能够在电视这样的大设施上运行。应用雷同的代码来适应不同的屏幕大小和像素密度是一个挑战。

Flutter响应式布局的设计没有硬性的规定。在本文中,我将向您展现在设计响应式布局时能够遵循的一些办法。

在应用Flutter构建响应式布局之前,我想阐明一下Android和iOS是如何解决不同屏幕大小的布局的。

Android的办法

为了解决不同的屏幕尺寸和像素密度,在Android中应用了以下概念:

1. ConstraintLayout

Android UI设计中引入的一个革命性的货色是ConstraintLayout。它能够用于创立灵便的、响应性强的UI设计,以适应不同的屏幕大小和尺寸。它容许您依据与布局中其余视图的空间关系来指定每个视图的地位和大小。

但这并不能解决大型设施的问题,在大型设施中,拉伸或只是调整UI组件的大小并不是利用屏幕面积的最优雅的形式。在屏幕面积很小的智能手表,调整组件以适应屏幕大小可能会导致奇怪的UI。

2. Alternative layouts

要解决上述问题,您能够为不同大小的设施应用alternative layouts。例如,你能够在平板电脑等设施上应用分屏视图来提供良好的用户体验,并明智地应用大屏幕。

在Android中,你能够为不同的屏幕大小定义不同的布局文件,Android框架会依据设施的屏幕大小主动解决这些布局之间的切换。

3. Fragments

应用Fragment,你能够将你的UI逻辑提取到独自的组件中,这样当你为大屏幕尺寸设计多窗格布局时,你不用独自定义逻辑。您能够重用为每个片段定义的Fragment。

4. Vector graphics

Vector graphics应用XML创立图像来定义门路和色彩,而不是应用像素位图。它能够缩放到任何大小。在Android中,你能够应用VectorDrawable来绘制任何类型的插图,比方图标。

iOS的办法

iOS用于定义响应式布局的形式如下

1. Auto Layout

Auto Layout可用于构建自适应界面,您能够在其中定义用于控制应用程序内容的规定(称为束缚)。 当检测到某些环境变动(称为特色)时,“Auto Layout”会依据指定的约束条件主动从新调整布局。

2. Size classes

Size类的特点是会依据其大小主动调配给内容区域。 iOS 会依据内容区域的Size类别动静地进行布局调整。在iPad上,size类也实用。

3. 一些UI 组件

还有一些其余的UI嘴贱你能够用来在iOS上构建响应式UI,像UIStackView, UIViewController,和UISplitViewController。

Flutter是如何自适应的

即便你不是Android或iOS的开发者,到目前为止,你应该曾经理解了这些平台是如何解决响应式布局的。

在Android中,要在单个屏幕上显示多个UI视图,请应用Fragments,它们相似于可在应用程序的Activity中运行的可重用组件。

您能够在一个Activity中运行多个Fragment,然而不能在一个应用程序中同时运行多个Activity。

在iOS中,为了管制多个视图控制器,应用了UISplitViewController,它在分层界面中治理子视图控制器。

当初咱们来到Flutter

Flutter引入了widget的概念。它们像积木一样拼凑在一起构建应用程序画面。

记住,在Flutter中,每个屏幕和整个应用程序也是一个widget!

widget实质上是可重用的,因而在Flutter中构建响应式布局时,您不须要学习任何其余概念。

Flutter的响应式概念

正如我后面所说的,我将探讨开发响应式布局所需的重要概念,而后你来抉择应用什么样的形式在你的APP上实现响应式布局。

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

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. 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的方向定义,如果一个小部件的严惩于高,他就是横向的,如果高大于宽,他就是横向的,仅此而已。

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所领有的全副空间就是纵轴的一半,理论他曾经填充了。

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

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,除了最初一个。

在构建一个示例响应式应用程序时,让咱们学习最初一个概念。

创立一个响应式APP

当初,咱们将利用上一节中形容的一些概念。与此同时,您还将学习为大屏幕构建布局的另一个重要概念,即分屏视图(一个屏幕上显示多个页面)。

响应式布局:在不同大小的屏幕上应用不同的布局。
咱们将建设一个名叫Flow的聊天应用程序。

app次要由两个局部组成:

  • HomePage (PeopleView, BookmarkView, ContactView)
  • ChatPage (PeopleView, ChatView)

对于大屏幕,咱们将显示蕴含MenuWidget和DestinationView的分屏视图。您能够看到,在Flutter中创立分屏视图是非常容易的,您只需应用一行将它们并排搁置,而后为了填满整个空间,只需应用Expanded widget包装两个视图。您还能够定义扩大小部件的flex属性,这将容许您指定每个小部件应该笼罩屏幕的多少局部(默认flex设置为1)。

然而,如果您当初挪动到一个特定的屏幕,而后在视图之间切换,那么您将失落页面的上下文,也就是说您将始终返回到第一个页面,即“聊天”。为了解决这个问题,我应用了多个回调函数来返回所选页面到主页。实际上,您应该应用状态治理技术来解决此场景。因为本文的惟一目标是教您构建响应式布局,所以我不探讨任何状态治理的复杂性。

响应式APP Github地址感兴趣的小伙伴能够看一看

上文中的所有布局组件的demo都在译者的github上

地址https://github.com/jack0-0wu/...

本文为medium翻译

原文地址

https://medium.com/flutter-co...
坐而论道不如起而行之