Flutter 笔记 | Flutter 布局组件

news2024/11/25 9:45:55

布局类组件都会包含一个或多个子组件,布局类组件都是直接或间接继承SingleChildRenderObjectWidgetMultiChildRenderObjectWidget的Widget,它们一般都会有一个childchildren属性用于接收子 Widget。

不同的布局类组件对子组件排列(layout)方式不同,如下表所示:

Widget说明用途
LeafRenderObjectWidget非容器类组件基类Widget树的叶子节点,用于没有子节点的widget,通常基础组件都属于这一类,如Image
SingleChildRenderObjectWidget单子组件基类包含一个子Widget,如:ConstrainedBoxDecoratedBox
MultiChildRenderObjectWidget多子组件基类包含多个子Widget,一般都有一个children参数,接受一个Widget数组。如Row、Column、Stack

我们看一下继承关系 Widget > RenderObjectWidget > (Leaf/SingleChild/MultiChild) RenderObjectWidget

RenderObjectWidget类中定义了创建、更新RenderObject的方法,子类必须实现他们,关于RenderObject我们现在只需要知道它是最终布局、渲染UI界面的对象即可,也就是说,对于布局类组件来说,其布局算法都是通过对应的RenderObject对象来实现的,所以如果对某个布局类组件的原理感兴趣,可以查看其对应的RenderObject的实现,比如Stack(层叠布局)对应的RenderObject对象就是RenderStack,而层叠布局的实现就在RenderStack中。

布局原理与约束

尺寸限制类容器用于限制容器大小,Flutter中提供了多种这样的容器,如ConstrainedBoxSizedBoxUnconstrainedBoxAspectRatio 等。

Flutter布局模型

Flutter 中有两种布局模型:

  1. 基于 RenderBox 的盒模型布局。
  2. 基于 Sliver ( RenderSliver ) 按需加载列表布局。

两种布局方式在细节上略有差异,但大体流程相同,布局流程如下:

  1. 上层组件向下层组件传递约束(constraints)条件。
  2. 下层组件确定自己的大小,然后告诉上层组件。注意下层组件的大小必须符合父组件的约束。
  3. 上层组件确定下层组件相对于自身的偏移和确定自身的大小(大多数情况下会根据子组件的大小来确定自身的大小)。

比如,父组件传递给子组件的约束是“最大宽高不能超过100,最小宽高为0”,如果我们给子组件设置宽高都为200,则子组件最终的大小是100*100,因为任何时候子组件都必须先遵守父组件的约束,在此基础上再应用子组件约束(相当于父组件的约束和自身的大小求一个交集)。

盒模型布局组件有两个特点:

  1. 组件对应的渲染对象都继承自 RenderBox 类。
  2. 在布局过程中父级传递给子级的约束信息由 BoxConstraints 描述。

BoxConstraints

BoxConstraints 是盒模型布局过程中父级组件传递给子组件的约束信息,用来描述子组件可用的空间范围,它包含最小和最大宽高信息,子组件大小需要在约束的范围内,BoxConstraints 默认的构造函数如下:

const BoxConstraints({
  this.minWidth = 0.0, //最小宽度
  this.maxWidth = double.infinity, //最大宽度
  this.minHeight = 0.0, //最小高度
  this.maxHeight = double.infinity //最大高度
})

它包含 4 个属性,BoxConstraints还定义了一些便捷的构造函数,用于快速生成特定限制规则的BoxConstraints,如BoxConstraints.tight(Size size),它可以生成固定宽高的限制;BoxConstraints.expand()可以生成一个尽可能大的用以填充另一个容器的BoxConstraints

约定:为了描述方便,如果我们说一个组件不约束其子组件或者取消对子组件约束时是指对子组件约束的最大宽高为无限大,而最小宽高为0,相当于子组件完全可以自己根据需要的空间来确定自己的大小。

ConstrainedBox

ConstrainedBox用于对子组件添加额外的约束。例如,如果你想让子组件的最小高度是80像素,你可以使用const BoxConstraints(minHeight: 80.0)作为子组件的约束。

示例

我们先定义一个redBox,它是一个背景颜色为红色的盒子,不指定它的宽度和高度:

Widget redBox = DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
);

我们实现一个最小高度为50,宽度尽可能大的红色容器。

ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: double.infinity, //宽度尽可能大
    minHeight: 50.0 //最小高度为50像素
  ),
  child: Container(
    height: 5.0, 
    child: redBox ,
  ),
)

效果:

在这里插入图片描述
可以看到,我们虽然将Container的高度设置为5像素,但是最终却是50像素,这正是ConstrainedBox最小高度限制生效了。如果将Container的高度设置为80像素,那么最终红色区域的高度也会是80像素,因为在此示例中,ConstrainedBox只限制了最小高度,并未限制最大高度

SizedBox

SizedBox用于给子元素指定固定的宽高,如:

SizedBox(
  width: 80.0,
  height: 80.0,
  child: redBox
)

效果:
在这里插入图片描述

实际上SizedBox只是ConstrainedBox的一个定制,上面代码等价于:

ConstrainedBox(
  constraints: BoxConstraints.tightFor(width: 80.0,height: 80.0),
  child: redBox, 
)

BoxConstraints.tightFor(width: 80.0,height: 80.0)等价于:

BoxConstraints(minHeight: 80.0,maxHeight: 80.0,minWidth: 80.0,maxWidth: 80.0)

而实际上ConstrainedBoxSizedBox都是通过RenderConstrainedBox来渲染的,我们可以看到ConstrainedBoxSizedBoxcreateRenderObject()方法都返回的是一个RenderConstrainedBox对象:


RenderConstrainedBox createRenderObject(BuildContext context) {
  return RenderConstrainedBox(
    additionalConstraints: ...,
  );
}

多重限制

如果某一个组件有多个父级ConstrainedBox限制,那么最终会是哪个生效?我们看一个例子:

ConstrainedBox(
  constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0), //父
  child: ConstrainedBox(
    constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),//子
    child: redBox,
  ),
)

上面我们有父子两个ConstrainedBox,他们的约束条件不同,运行效果:
在这里插入图片描述
最终显示效果是宽90,高60,也就是说是子ConstrainedBoxminWidth生效,而minHeight是父ConstrainedBox生效。单凭这个例子,我们还总结不出什么规律,我们将上例中父子约束条件换一下:

ConstrainedBox(
  constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),
  child: ConstrainedBox(
    constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0),
    child: redBox,
  )
)

运行效果:

在这里插入图片描述

最终的显示效果仍然是90,高60,效果相同,但意义不同,因为此时minWidth生效的是父ConstrainedBox,而minHeight是子ConstrainedBox生效。

通过上面示例,我们发现有多重限制时,对于minWidthminHeight来说,是取父子中相应数值较大的。实际上,只有这样才能保证父限制与子限制不冲突。

UnconstrainedBox

虽然任何时候子组件都必须遵守其父组件的约束,但前提条件是它们必须是父子关系,假如有一个组件 A,它的子组件是BB 的子组件是 C,则 C 必须遵守 B 的约束,同时 B 必须遵守 A 的约束,但是 A 的约束不会直接约束到 C,除非BA对它自己的约束透传给了C。 利用这个原理,就可以实现一个这样的 B 组件:

  1. B 组件中在布局 C 时不约束C(可以为无限大)。
  2. C 根据自身真实的空间占用来确定自身的大小。
  3. B 在遵守 A 的约束前提下结合子组件的大小确定自身大小。

而这个 B 组件就是 UnconstrainedBox 组件,也就是说UnconstrainedBox 的子组件将不再受到约束,大小完全取决于自己。一般情况下,我们会很少直接使用此组件,但在 "去除"多重限制 的时候也许会有帮助,我们看下下面的代码:

ConstrainedBox(
  constraints: BoxConstraints(minWidth: 60.0, minHeight: 100.0),  //父
  child: UnconstrainedBox( //“去除”父级限制
    child: ConstrainedBox(
      constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),//子
      child: redBox,
    ),
  )
)

上面代码中,如果没有中间的UnconstrainedBox,那么根据上面所述的多重限制规则,那么最终将显示一个90×100的红色框。但是由于UnconstrainedBox去除”了父ConstrainedBox的限制,则最终会按照子ConstrainedBox的限制来绘制redBox,即90×20,如图所示:

在这里插入图片描述

但是,请注意,UnconstrainedBox对父组件限制的“去除”并非是真正的去除:上面例子中虽然红色区域大小是90×20,但上方仍然有80的空白空间。也就是说父限制的minHeight(100.0)仍然是生效的,只不过它不影响最终子元素redBox的大小,但仍然还是占有相应的空间,可以认为此时的父ConstrainedBox是作用于子UnconstrainedBox上,而redBox只受子ConstrainedBox限制,这一点请务必注意。

那么有什么方法可以彻底去除父ConstrainedBox的限制吗?答案是否定的!请牢记,任何时候子组件都必须遵守其父组件的约束,所以在定义一个通用的组件时,如果要对子组件指定约束,那么一定要注意,因为一旦指定约束条件,子组件自身就不能违反约束。

在实际开发中,当我们发现已经使用 SizedBoxConstrainedBox给子元素指定了固定宽高,但是仍然没有效果时,几乎可以断定:已经有父组件指定了约束

举个例子,如 Material 组件库中的AppBar(导航栏)的右侧菜单中,我们使用SizedBox指定了 loading 按钮的大小,代码如下:

 AppBar(
   title: Text(title),
   actions: <Widget>[
     SizedBox(
       width: 20, 
       height: 20,
       child: CircularProgressIndicator(
         strokeWidth: 3,
         valueColor: AlwaysStoppedAnimation(Colors.white70),
       ),
     )
   ],
)

运行效果:

在这里插入图片描述

我们会发现右侧loading按钮大小并没有发生变化!这正是因为AppBar中已经指定了actions按钮的约束条件,所以我们要自定义loading按钮大小,就必须通过UnconstrainedBox来 “去除” 父元素的限制,代码如下:

AppBar(
  title: Text(title),
  actions: <Widget>[
    UnconstrainedBox(
      child: SizedBox(
        width: 20,
        height: 20,
        child: CircularProgressIndicator(
          strokeWidth: 3,
          valueColor: AlwaysStoppedAnimation(Colors.white70),
        ),
      ),
    )
  ],
)

运行效果:

在这里插入图片描述

可以看到生效了!实际上将 UnconstrainedBox 换成 Center 或者 Align 也是可以的。

另外,需要注意,UnconstrainedBox 虽然在其子组件布局时可以取消约束(子组件可以为无限大),但是 UnconstrainedBox 自身是受其父组件约束的,所以当 UnconstrainedBox 随着其子组件变大后,如果UnconstrainedBox 的大小超过它父组件约束时,也会导致溢出报错,比如:

Column(
  children: <Widget>[
    UnconstrainedBox(
      alignment: Alignment.topLeft,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(children: [Text('xx' * 30)]),
      ),
    ),
 ]

运行效果:

在这里插入图片描述

文本已经超过屏幕宽度,溢出了。

AspectRatio

AspectRatio的作用是可以调整子元素child的宽高比

AspectRatio首先会在布局限制条件允许的范围内尽可能的扩展,widget的高度是由宽度和比率决定的,类似于BoxFit中的contain,按照固定比率去尽量占满区域。

如果在满足所有限制条件过后无法找到一个可行的尺寸,AspectRatio最终将会去优先适应布局限制条件,而忽略所设置的比率。

属性说明
aspectRatio宽高比,最终可能不会根据这个值去布局,具体则要看综合因素,外层是否允许按照这种比率进行布局,这只是一个参考值
child子组件

示例:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(title: const Text("Flutter App")),
        body: const LayoutDemo(),
      ),
    );
  }
}

//需求:页面上显示一个容器,宽度是屏幕的宽度,高度是容器宽度的一半
class LayoutDemo extends StatelessWidget {
  const LayoutDemo({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {    
    return AspectRatio(
      aspectRatio: 2/1,
      child: Container(
        color: Colors.red,
      ),
    );
  }
}

在这里插入图片描述

LimitedBox

LimitedBox用于指定child的最大宽高,它可以将child限制在其设定的最大宽高中的,但是这个限定是有条件的。当LimitedBox最大宽度不受限制时,child的宽度就会受到这个最大宽度的限制,同理高度。

示例:

 LimitedBox(
   maxWidth: 100,
   maxHeight: 100,
   child:  Container(
     color: Colors.blue,
     width: 200.0,
   ),
 ),

FractionallySizedBox

FractionallySizedBox 的作用是使子组件宽高可以根据父容器宽高的百分比来设置。

示例:

class FractionallySizedBoxWidget extends StatelessWidget {
  const FractionallySizedBoxWidget({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
     return Scaffold(
       appBar: AppBar(title: const Text("FractionallySizedBox"),),
       body: Center(
         //child宽高分别是父组件宽高的80%
         child: FractionallySizedBox(
           widthFactor: 0.8,
           heightFactor: 0.8,
           child: Container(
             color: Colors.red,
           ),
         ),
       ),
     );
  }
}

在这里插入图片描述

线性布局(Row和Column)

所谓线性布局,即指沿水平或垂直方向排列子组件。Flutter 中通过RowColumn来实现线性布局,类似于Android 中的LinearLayout控件。RowColumn都继承自Flex

主轴和纵轴

对于线性布局,有主轴和纵轴之分,如果布局是沿水平方向,那么主轴就是指水平方向,而纵轴即垂直方向;如果布局沿垂直方向,那么主轴就是指垂直方向,而纵轴就是水平方向。其实纵轴就是相对于主轴方向的交叉轴。

在线性布局中,有两个定义对齐方式的枚举类MainAxisAlignmentCrossAxisAlignment,分别代表主轴对齐和交叉轴对齐。

Row

Row可以沿水平方向排列其子widget。常用属性:

属性说明
textDirection表示水平方向子组件的布局顺序(是从左往右还是从右往左),默认为系统当前Locale环境的文本方向(如中文、英语都是从左往右,而阿拉伯语是从右往左)
mainAxisSize表示在主轴(水平)方向占用的空间,默认是MainAxisSize.max,表示尽可能多的占用水平方向的空间,此时无论子 widgets 实际占用多少水平空间,Row的宽度始终等于水平方向的最大宽度;而MainAxisSize.min表示尽可能少的占用水平空间,当子组件没有占满水平剩余空间,则Row的实际宽度等于所有子组件占用的的水平空间
mainAxisAlignment表示子组件在Row的水平方向的对齐方式,如果mainAxisSize值为MainAxisSize.min,则此属性无意义,因为子组件的宽度等于Row的宽度。只有当mainAxisSize的值为MainAxisSize.max时,此属性才有意义
verticalDirection表示Row交叉轴(垂直)的对齐方向,默认是VerticalDirection.down,表示从上到下
crossAxisAlignment表示子组件在交叉轴方向的对齐方式,Row的高度等于子组件中最高的子元素高度,它的取值和MainAxisAlignment一样(包含start、end、 center三个值),不同的是crossAxisAlignment的参考系是verticalDirection,即verticalDirection值为VerticalDirection.downcrossAxisAlignment.start指顶部对齐,verticalDirection值为VerticalDirection.up时,crossAxisAlignment.start指底部对齐
children子组件数组

其中 mainAxisAlignment 的取值如下:

MainAxisAlignment说明
MainAxisAlignment.start表示沿textDirection的初始方向对齐,如textDirection取值为TextDirection.ltr时,则MainAxisAlignment.start表示左对齐,textDirection取值为TextDirection.rtl时表示从右对齐
MainAxisAlignment.endMainAxisAlignment.start正好相反
MainAxisAlignment.center表示居中对齐
MainAxisAlignment.spaceBetween将空闲空间均匀地分布在子组件之间
MainAxisAlignment.spaceAround将空闲空间均匀地分布在子组件之间,并且在第一个child之前和最后一个child之后显示均分空间的一半
MainAxisAlignment.spaceEvenly将空闲空间均匀地分布在子组件之间,并且在第一个child之前和最后一个child之后也显示均分的空间

可以这么理解:textDirectionmainAxisAlignment的参考系。

示例:

Column(
  //测试Row对齐方式,排除Column默认居中对齐的干扰
  crossAxisAlignment: CrossAxisAlignment.start,
  children: <Widget>[
    Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(" hello world "),
        Text(" I am Jack "),
      ],
    ),
    Row(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(" hello world "),
        Text(" I am Jack "),
      ],
    ),
    Row(
      mainAxisAlignment: MainAxisAlignment.end,
      textDirection: TextDirection.rtl,
      children: <Widget>[
        Text(" hello world "),
        Text(" I am Jack "),
      ],
    ),
    Row(
      crossAxisAlignment: CrossAxisAlignment.start,  
      verticalDirection: VerticalDirection.up,
      children: <Widget>[
        Text(" hello world ", style: TextStyle(fontSize: 30.0),),
        Text(" I am Jack "),
      ],
    ),
  ],
);

效果:

在这里插入图片描述
解释:

  • 第一个 Row 很简单,默认为居中对齐;
  • 第二个 Row ,由于mainAxisSize值为MainAxisSize.minRow 的宽度等于两个Text的宽度和,所以对齐是无意义的,所以会从左往右显示;
  • 第三个 Row 设置textDirection值为TextDirection.rtl,所以子组件会从右向左的顺序排列,而此时MainAxisAlignment.end表示左对齐,所以最终显示结果就是图中第三行的样子;
  • 第四个 Row 测试的是纵轴的对齐方式,由于两个子 Text 字体不一样,所以其高度也不同,我们指定了verticalDirection值为VerticalDirection.up,即从低向顶排列,而此时crossAxisAlignment值为CrossAxisAlignment.start表示底对齐。

Column

Column可以在垂直方向排列其子组件。参数和Row一样,不同的是布局方向为垂直。

示例:

import 'package:flutter/material.dart';

class CenterColumnRoute extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.center,
      children: <Widget>[
        Text("hi"),
        Text("world"),
      ],
    );
  }
}

效果:

在这里插入图片描述

解释:

  • 由于我们没有指定ColumnmainAxisSize,所以使用默认值MainAxisSize.max,则Column会在垂直方向占用尽可能多的空间,此例中会占满整个屏幕高度。
  • 由于我们指定了 crossAxisAlignment 属性为CrossAxisAlignment.center,那么子项在Column交叉轴方向(水平方向)会居中对齐。注意,在水平方向对齐是有边界的,总宽度为Column占用空间的实际宽度,而实际的宽度取决于子项中宽度最大Widget。在本例中,Column有两个子Widget,而显示“world”的Text宽度最大,所以Column的实际宽度则为Text("world") 的宽度,所以居中对齐后Text("hi")会显示在Text("world")的中间部分。

实际上,RowColumn都只会在主轴方向占用尽可能大的空间,而在交叉轴的长度则取决于他们最大子元素的长度

如果我们想让本例中的两个文本控件在整个手机屏幕中间对齐,我们有两种方法:

  1. Column的宽度指定为屏幕宽度;这很简单,我们可以通过ConstrainedBoxSizedBox来强制更改宽度限制。

    例如:将ConstrainedBoxminWidth设为double.infinity,可以使宽度占用尽可能多的空间。

ConstrainedBox(	
  constraints: BoxConstraints(minWidth: double.infinity), 
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.center,
    children: <Widget>[
      Text("hi"),
      Text("world"),
    ],
  ),
);

在这里插入图片描述

  1. 可以使用Center 组件

嵌套情况

如果Row里面嵌套Row,或者Column里面再嵌套Column,那么只有最外面的RowColumn会占用尽可能大的空间,里面RowColumn所占用的空间为实际大小,下面以Column为例说明:

Container(
  color: Colors.green,
  child: Padding(
    padding: const EdgeInsets.all(16.0),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.max, //有效,外层Colum高度为整个屏幕
      children: <Widget>[
        Container(
          color: Colors.red,
          child: Column(
            mainAxisSize: MainAxisSize.max,//无效,内层Colum高度为实际高度  
            children: <Widget>[
              Text("hello world "),
              Text("I am Jack "),
            ],
          ),
        )
      ],
    ),
  ),
);

运行效果:

在这里插入图片描述

如果要让里面的Column占满外部Column,可以使用Expanded 组件:

Expanded( 
  child: Container(
    color: Colors.red,
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center, //垂直方向居中对齐
      children: <Widget>[
        Text("hello world "),
        Text("I am Jack "),
      ],
    ),
  ),
)

运行效果:

在这里插入图片描述

弹性布局(Flex、Expanded)

弹性布局允许子组件按照一定比例来分配父容器空间。弹性布局的概念在其他UI系统中也都存在,如 H5 中的弹性盒子布局,Android 中的FlexboxLayout等。Flutter 中的弹性布局主要通过FlexExpanded来配合实现。

Flex

Flex组件可以沿着水平或垂直方向排列子组件,如果你知道主轴方向,可以直接使用RowColumn会方便一些,因为它们都继承自Flex,参数基本相同,所以能使用Flex的地方基本上都可以使用RowColumn

Flex本身功能是很强大的,它也可以和Expanded组件配合实现弹性布局。接下来我们只讨论Flex和弹性布局相关的属性(其他属性已经在介绍RowColumn时介绍过了)。

Flex({
  ...
  required this.direction, //弹性布局的方向, Row默认为水平方向,Column默认为垂直方向
  List<Widget> children = const <Widget>[],
})

Flex继承自MultiChildRenderObjectWidget,对应的RenderObjectRenderFlexRenderFlex中实现了其布局算法。

Expanded

Expanded 只能作为 Flex 的孩子(否则会报错),它可以按比例“扩伸”Flex子组件所占用的空间。因为 RowColumn 都继承自 Flex,所以 Expanded 也可以作为它们的孩子。

const Expanded({
  int flex = 1, 
  required Widget child,
})
  • flex:弹性系数,如果为 0null,则child是没有弹性的,即不会被扩伸占用的空间。如果大于0,所有的Expanded按照其 flex 的比例来分割主轴的全部空闲空间。

下面我们看一个例子:

class FlexLayoutTestRoute extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        //Flex的两个子widget按1:2来占据水平空间  
        Flex(
          direction: Axis.horizontal,
          children: <Widget>[
            Expanded(
              flex: 1,
              child: Container(
                height: 30.0,
                color: Colors.red,
              ),
            ),
            Expanded(
              flex: 2,
              child: Container(
                height: 30.0,
                color: Colors.green,
              ),
            ),
          ],
        ),
        Padding(
          padding: const EdgeInsets.only(top: 20.0),
          child: SizedBox(
            height: 100.0,
            //Flex的三个子widget,在垂直方向按2:1:1来占用100像素的空间  
            child: Flex(
              direction: Axis.vertical,
              children: <Widget>[
                Expanded(
                  flex: 2,
                  child: Container(
                    height: 30.0,
                    color: Colors.red,
                  ),
                ),
                Spacer(
                  flex: 1,
                ),
                Expanded(
                  flex: 1,
                  child: Container(
                    height: 30.0,
                    color: Colors.green,
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }
}

运行效果:

在这里插入图片描述

示例中的Spacer的功能是占用指定比例的空间,实际上它只是Expanded的一个包装类,Spacer的源码如下:

class Spacer extends StatelessWidget {
  const Spacer({Key? key, this.flex = 1})
    : assert(flex != null),
      assert(flex > 0),
      super(key: key);
  
  final int flex;

  
  Widget build(BuildContext context) {
    return Expanded(
      flex: flex,
      child: const SizedBox.shrink(),
    );
  }
}

流式布局(Wrap、Flow)

在介绍 RowColum 时,如果子 widget 超出屏幕范围,则会报溢出错误,如:

Row(
  children: <Widget>[
    Text("xxx"*100)
  ],
);

在这里插入图片描述

可以看到,右边溢出部分报错。这是因为Row默认只有一行,超出屏幕后不会折行。我们把超出屏幕显示范围会自动折行的布局称为流式布局。Flutter中通过WrapFlow来支持流式布局,将上例中的 Row 换成Wrap后溢出部分则会自动折行,下面我们分别介绍WrapFlow

Wrap

WrapRow\Column 的大部分属性类似,RowColumn都是单行单列的,Wrap则突破了这个限制,当mainAxis上空间不足时,则向crossAxis上去扩展显示。

下面是Wrap常用的几个属性:

属性说明
direction主轴的方向,默认水平Axis.horizontal
alignment主轴的对其方式,默认WrapAlignment.start
textDirection文本方向
verticalDirection定义了children摆放顺序,默认是VerticalDirection.down,同Flex相关属性
spacing主轴方向子widget的间距
runSpacing交叉轴方向的间距
runAlignment交叉轴方向的对齐方式

示例:

Wrap(
   spacing: 8.0, // 主轴(水平)方向间距
   runSpacing: 4.0, // 交叉轴(垂直)方向间距
   alignment: WrapAlignment.center, //沿主轴方向居中
   children: <Widget>[
     Chip(
       avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('A')),
       label: Text('Hamilton'),
     ),
     Chip(
       avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('M')),
       label: Text('Lafayette'),
     ),
     Chip(
       avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('H')),
       label: Text('Mulligan'),
     ),
     Chip(
       avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('J')),
       label: Text('Laurens'),
     ),
   ],
)

效果:
在这里插入图片描述

Flow

我们一般很少会使用Flow,因为其过于复杂,需要自己实现子 widget 的位置转换,在很多场景下首先要考虑的是Wrap是否满足需求。Flow主要用于一些需要自定义布局策略或性能要求较高(如动画中)的场景。Flow有如下优点:

  • 性能好;Flow是一个对子组件尺寸以及位置调整非常高效的控件,Flow用转换矩阵在对子组件进行位置调整的时候进行了优化:在Flow定位过后,如果子组件的尺寸或者位置发生了变化,在FlowDelegate中的paintChildren()方法中调用context.paintChild 进行重绘,而context.paintChild在重绘时使用了转换矩阵,并没有实际调整组件位置。
  • 灵活;由于我们需要自己实现FlowDelegatepaintChildren()方法,所以我们需要自己计算每一个组件的位置,因此,可以自定义布局策略。

缺点:

  • 使用复杂。
  • Flow 不能自适应子组件大小,必须通过指定父容器大小或实现TestFlowDelegategetSize返回固定大小。

示例:我们对六个色块进行自定义流式布局:

Flow(
  delegate: TestFlowDelegate(margin: EdgeInsets.all(10.0)),
  children: <Widget>[
    Container(width: 80.0, height:80.0, color: Colors.red,),
    Container(width: 80.0, height:80.0, color: Colors.green,),
    Container(width: 80.0, height:80.0, color: Colors.blue,),
    Container(width: 80.0, height:80.0,  color: Colors.yellow,),
    Container(width: 80.0, height:80.0, color: Colors.brown,),
    Container(width: 80.0, height:80.0,  color: Colors.purple,),
  ],
)

实现TestFlowDelegate:

class TestFlowDelegate extends FlowDelegate {
  EdgeInsets margin;

  TestFlowDelegate({this.margin = EdgeInsets.zero});

  double width = 0;
  double height = 0;

  
  void paintChildren(FlowPaintingContext context) {
    var x = margin.left;
    var y = margin.top;
    //计算每一个子widget的位置
    for (int i = 0; i < context.childCount; i++) {
      var w = context.getChildSize(i)!.width + x + margin.right;
      if (w < context.size.width) {
        context.paintChild(i, transform: Matrix4.translationValues(x, y, 0.0));
        x = w + margin.left;
      } else {
        x = margin.left;
        y += context.getChildSize(i)!.height + margin.top + margin.bottom;
        //绘制子widget(有优化)
        context.paintChild(i, transform: Matrix4.translationValues(x, y, 0.0));
        x += context.getChildSize(i)!.width + margin.left + margin.right;
      }
    }
  }

  
  Size getSize(BoxConstraints constraints) {
    // 指定Flow的大小,简单起见我们让宽度竟可能大,但高度指定为200,
    // 实际开发中我们需要根据子元素所占用的具体宽高来设置Flow大小
    return Size(double.infinity, 200.0);
  }

  
  bool shouldRepaint(FlowDelegate oldDelegate) {
    return oldDelegate != this;
  }
}

运行效果:

在这里插入图片描述

可以看到我们主要的任务就是实现paintChildren,它的主要任务是确定每个子widget位置。由于Flow不能自适应子widget的大小,我们通过在getSize返回一个固定大小来指定Flow的大小。

注意,如果我们需要自定义布局策略,一般首选的方式是通过直接继承RenderObject,然后通过重写 performLayout 的方式实现。

层叠布局(Stack、Positioned)

层叠布局和 Web 中的绝对定位、Android 中的 FrameLayout 相似,子组件可以根据距父容器四个角的位置来确定自身的位置。子组件是按照代码中声明的顺序堆叠起来。Flutter中使用Stack结合PositionedAlign这两个组件来配合实现定位。Stack允许子组件堆叠,而Positioned用于根据Stack的四个角来确定子组件的位置。

Stack

常用属性:

属性说明
alignment此参数决定如何去对齐没有定位(没有使用Positioned)或部分定位的子组件。
所谓部分定位,在这里特指没有在某一个轴上定位:leftright为横轴,topbottom为纵轴,只要包含某个轴上的一个定位属性就算在该轴上有定位。默认值是AlignmentDirectional.topStart
textDirectionRowWraptextDirection功能一样,都用于确定alignment对齐的参考系,
即:textDirection的值为TextDirection.ltr,则alignmentstart代表左,end代表右,即从左往右的顺序;
textDirection的值为TextDirection.rtl,则alignmentstart代表右,end代表左,即从右往左的顺序
fit此参数用于确定没有定位的子组件如何去适应Stack的大小。StackFit.loose表示使用子组件的大小,StackFit.expand表示扩伸到Stack的大小,默认是StackFit.loose
clipBehavior此属性决定对超出Stack显示空间的部分如何剪裁,Clip枚举类中定义了剪裁的方式,默认是Clip.hardEdge 表示直接剪裁,不应用抗锯齿

Positioned

常用属性:

属性说明
left子元素距离左侧距离
top子元素距离顶部的距离
right子元素距离右侧距离
bottom子元素距离底部的距离
child子组件
width**子组件的高度 **
height子组件的高度

注意,Positionedwidthheight 和其他地方的意义稍微有点区别,此处用于配合left、top 、right、 bottom来定位组件,举个例子,在水平方向时,你只能指定left、right、width三个属性中的两个,如指定leftwidth后,right会自动算出(left+width),如果同时指定三个属性则会报错,垂直方向同理。另外,宽度和高度必须是固定值,没法使用double.infinity

示例:下面代码通过对几个Text组件的定位来演示StackPositioned的特性

//通过ConstrainedBox来确保Stack占满屏幕
ConstrainedBox(
  constraints: BoxConstraints.expand(),
  child: Stack(
    alignment:Alignment.center , //指定未定位或部分定位widget的对齐方式
    //注意:相对于外部容器进行定位,如果没有外部容器就相对于整个屏幕进行定位
    children: <Widget>[
      Container(
        child: Text("Hello world",style: TextStyle(color: Colors.white)),
        color: Colors.red,
      ),
      Positioned(
        left: 18.0,
        child: Text("I am Jack"),
      ),
      Positioned(
        top: 18.0,
        child: Text("Your friend"),
      )        
    ],
  ),
);

效果:

在这里插入图片描述

  • 由于第一个子文本组件Text("Hello world")没有指定定位,并且alignment值为Alignment.center,所以它会居中显示。
  • 第二个子文本组件Text("I am Jack")只指定了水平方向的定位(left),所以属于部分定位,即垂直方向上没有定位,那么它在垂直方向的对齐方式则会按照alignment指定的对齐方式对齐,即垂直方向居中。
  • 对于第三个子文本组件Text("Your friend"),和第二个Text原理一样,只不过是水平方向没有定位,则水平方向居中。

我们给上例中的Stack指定一个fit属性,然后将三个子文本组件的顺序调整一下:

Stack(
  alignment:Alignment.center ,
  fit: StackFit.expand, //未定位widget占满Stack整个空间
  children: <Widget>[
    Positioned(
      left: 18.0,
      child: Text("I am Jack"),
    ),
    Container(child: Text("Hello world",style: TextStyle(color: Colors.white)),
      color: Colors.red,
    ),
    Positioned(
      top: 18.0,
      child: Text("Your friend"),
    )
  ],
),

效果:

在这里插入图片描述

可以看到,由于第二个子文本组件没有定位,所以fit属性会对它起作用,就会占满Stack。由于Stack子元素是堆叠的,所以第一个子文本组件被第二个遮住了,而第三个在最上层,所以可以正常显示。

StackPositioned实现固定导航案例

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    home: Scaffold(
        appBar: AppBar(title: const Text("你好Flutter")), body: const HomePage()),
  ));
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    //获取设备的宽度和高度
    final size = MediaQuery.of(context).size;
    return Stack(
      children: [
        ListView(
          padding: const EdgeInsets.only(top: 50),
          children: List.generate(
              40, (index) => ListTile(title: Text("我是一个列表$index"))),
        ),
        Positioned(
            left: 0,
            top: 0,
            // bottom: 0, // 改为bottom即底部固定
            width: size.width, //配置子元素的宽度和高度  没法使用double.infinity
            height: 44, //配置子元素的宽度和高度
            child: Container(
              alignment: Alignment.center,
              color: Colors.red,
              child: const Text(
                "二级导航",
                style: TextStyle(color: Colors.white),
              ),
            ))
      ],
    );
  }
}

效果:

在这里插入图片描述
上面代码中MediaQuery可以到获取屏幕宽度和高度,可以在build方法中调用:

Widget build(BuildContext context) {
	final size =MediaQuery.of(context).size;
	final width =size.width;
	final height =size.height;
	...
}

对齐与相对定位(Align)

通过StackPositioned,我们可以指定多个子元素相对于父元素各个边的精确偏移,并且可以重叠。但如果我们只想简单的调整一个子元素在父元素中的位置的话,使用 Align 组件会更简单一些。

属性说明
alignment需要一个AlignmentGeometry类型的值,表示子组件在父组件中的起始位置。AlignmentGeometry 是一个抽象类,它有两个常用的子类:AlignmentFractionalOffset
widthFactorheightFactor用于确定 Align 组件本身宽高的属性;它们是两个缩放因子,会分别乘以子元素的宽、高,最终的结果就是 Align 组件的宽高。如果值为null,则组件的宽高将会占用尽可能多的空间。

简单示例:

Container(
  height: 120.0,
  width: 120.0,
  color: Colors.blue.shade50,
  child: Align(
    alignment: Alignment.topRight,
    child: FlutterLogo(size: 60),
  ),
)

效果:

在这里插入图片描述

FlutterLogo 是 Flutter SDK 提供的一个组件,内容就是 Flutter 的 logo 。在上面的例子中,我们显式指定了Container的宽、高都为 120。如果我们不显式指定宽高,而通过同时指定widthFactorheightFactor2 也是可以达到同样的效果:

Align(
  widthFactor: 2,
  heightFactor: 2,
  alignment: Alignment.topRight,
  child: FlutterLogo(size: 60),
),

因为FlutterLogo的宽高为 60,则Align的最终宽高都为2*60=120

另外,我们还通过Alignment.topRight将FlutterLogo定位在Container的右上角。那Alignment.topRight是什么呢?通过源码我们可以看到其定义如下:

static const Alignment topRight = Alignment(1.0, -1.0);

可以看到它只是Alignment的一个实例,下面我们介绍一下Alignment

Alignment

Alignment继承自AlignmentGeometry,表示矩形内的一个点,他有两个属性x、y,分别表示在水平和垂直方向的偏移,Alignment定义如下:

Alignment(this.x, this.y)

Alignment Widget会以矩形的中心点作为坐标原点,即Alignment(0.0, 0.0)x、y的值从-11分别代表矩形左边到右边的距离和顶部到底边的距离,因此2个水平(或垂直)单位则等于矩形的宽(或高),如Alignment(-1.0, -1.0) 代表矩形的左侧顶点,而Alignment(1.0, 1.0)代表右侧底部终点,而Alignment(1.0, -1.0) 则正是右侧顶点,即Alignment.topRight。为了使用方便,矩形的原点、四个顶点,以及四条边的终点在Alignment类中都已经定义为了静态常量。

在这里插入图片描述
Alignment可以通过其坐标转换公式将其坐标转为子元素的具体偏移坐标:

(Alignment.x * childWidth / 2 + childWidth / 2, Alignment.y * childHeight / 2 + childHeight / 2)

其中childWidth为子元素的宽度,childHeight为子元素高度。

现在我们再看看上面的示例,我们将Alignment(1.0, -1.0)带入上面公式,可得FlutterLogo的实际偏移坐标正是(60,0)

下面再看一个例子:

Align(
  widthFactor: 2,
  heightFactor: 2,
  alignment: Alignment(2,0.0),
  child: FlutterLogo(size: 60),
)

我们可以先想象一下运行效果:将Alignment(2,0.0)带入上述坐标转换公式,可以得到FlutterLogo的实际偏移坐标为(90,30)。实际运行如图所示:

在这里插入图片描述

FractionalOffset

FractionalOffset 继承自 Alignment,它和 Alignment唯一的区别就是坐标原点不同!FractionalOffset 的坐标原点为矩形的左侧顶点,这和布局系统的一致,所以理解起来会比较容易。FractionalOffset的坐标转换公式为:

实际偏移 = (FractionalOffse.x * childWidth, FractionalOffse.y * childHeight)

简单示例:

Container(
  height: 120.0,
  width: 120.0,
  color: Colors.blue[50],
  child: Align(
    alignment: FractionalOffset(0.2, 0.6),
    child: FlutterLogo(size: 60),
  ),
)

效果:

在这里插入图片描述

我们将FractionalOffset(0.2, 0.6)带入坐标转换公式得FlutterLogo实际偏移为(12,36),和实际运行效果吻合。

建议在需要制定一些精确的偏移时应优先使用FractionalOffset,因为它的坐标原点和布局系统相同,能更容易算出实际偏移。

Align和Stack对比

可以看到,AlignStack/Positioned都可以用于指定子元素相对于父元素的偏移,但它们还是有两个主要区别:

  1. 定位参考系统不同Stack/Positioned定位的的参考系可以是父容器矩形的四个顶点;而Align则需要先通过 alignment 参数来确定坐标原点,不同的alignment会对应不同原点,最终的偏移是需要通过alignment的转换公式来计算出。
  2. Stack可以有多个子元素,并且子元素可以堆叠,而Align只能有一个子元素,不存在堆叠。

Align结合Stack使用:

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        height: 400,
        width: 300,
        color: Colors.red,
        child: const Stack(
          // alignment: Alignment.center,
          children: <Widget>[
            Align(
              alignment: Alignment(1,-0.2),
              child: Icon(Icons.home,size: 40,color: Colors.white),
            ),
            Align(
              alignment: Alignment.center,
              child: Icon(Icons.search,size: 30,color: Colors.white),
            ),
            Align(
              alignment: Alignment.bottomRight,
              child: Icon(Icons.settings_applications,size: 30,color:
              Colors.white),
            )
          ],
        ),
      ),
    );
  }
} 

Center组件

Center组件的源码定义如下:

class Center extends Align {
  const Center({ Key? key, double widthFactor, double heightFactor, Widget? child })
    : super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child);
}

可以看到Center继承自Align,它比Align只少了一个alignment 参数;由于Align的构造函数中alignment 默认值为Alignment.center,所以,我们可以认为Center组件其实是对齐方式确定(Alignment.center)了的Align

上面我们讲过当widthFactorheightFactornull时组件的宽高将会占用尽可能多的空间,这一点需要特别注意,我们通过一个示例说明:

...//省略无关代码
DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
  child: Center(
    child: Text("xxx"),
  ),
),
DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
  child: Center(
    widthFactor: 1,
    heightFactor: 1,
    child: Text("xxx"),
  ),
)

效果:
在这里插入图片描述

熟悉Web开发的同学可能会发现Align组件的特性和Web开发中相对定位(position: relative)非常像,是的!在大多数时候,我们可以直接使用Align组件来实现Web中相对定位的效果。

Card 组件

Card是卡片组件块,内容可以由大多数类型的Widget构成,Card具有圆角和阴影,这让它看起来有立体感。

属性说明
margin外边距
child子组件
elevation阴影值的深度
color背景颜色
shadowColor阴影颜色
margin外边距
clipBehavior内容溢出的剪切方式
Clip.none不剪切
Clip.hardEdge裁剪但不应用抗锯齿
Clip.antiAlias裁剪而且抗锯齿
Clip.antiAliasWithSaveLayer带有抗锯齿的剪辑,并在剪辑之后立即保存saveLayer
ShapeCard的阴影效果,默认的阴影效果为圆角的长方形边。
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))
),

Card 实现一个通讯录的卡片效果:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(title: const Text("Flutter App")),
        body: const LayoutDemo(),
      ),
    );
  }
}

class LayoutDemo extends StatelessWidget {
  const LayoutDemo({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    return ListView(
      children: [
        Card(
          shape: RoundedRectangleBorder(
              //Card的阴影效果
              borderRadius: BorderRadius.circular(10)),
          elevation: 20, //阴影值的深度
          margin: const EdgeInsets.all(10),
          child: Column(
            children: const [
              ListTile(
                title: Text("张三", style: TextStyle(fontSize: 28)),
                subtitle: Text("高级软件工程师"),
              ),
              Divider(),
              ListTile(
                title: Text("电话:152222222"),
              ),
              ListTile(
                title: Text("地址:北京市海淀区 xxx"),
              ),
            ],
          ),
        ),
        Card(
          shape: RoundedRectangleBorder(
              //Card的阴影效果
              borderRadius: BorderRadius.circular(10)),
          elevation: 20,
          margin: const EdgeInsets.all(10),
          // color:Colors.black12,  //背景颜色
          child: Column(
            children: const [
              ListTile(
                title: Text("李四", style: TextStyle(fontSize: 28)),
                subtitle: Text("Flutter高级软件工程师"),
              ),
              Divider(),
              ListTile(
                title: Text("电话:152222222"),
              ),
              ListTile(
                title: Text("地址:北京市海淀区 xxx"),
              ),
            ],
          ),
        ),
         Card(
          shape: RoundedRectangleBorder(
              //Card的阴影效果
              borderRadius: BorderRadius.circular(10)),
          elevation: 20, //阴影值的深度
          margin: const EdgeInsets.all(10),
          child: Column(
            children: const [
              ListTile(
                title: Text("张三", style: TextStyle(fontSize: 28)),
                subtitle: Text("高级软件工程师"),
              ),
              Divider(),
              ListTile(
                title: Text("电话:152222222"),
              ),
              ListTile(
                title: Text("地址:北京市海淀区 xxx"),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

在这里插入图片描述

Card 实现一个图文列表卡片效果:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(title: const Text("Flutter App")),
        body: const LayoutDemo(),
      ),
    );
  }
}

class LayoutDemo extends StatelessWidget {
  const LayoutDemo({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    return ListView(
      children: [
        Card(
          shape: RoundedRectangleBorder(
            borderRadius:BorderRadius.circular(10)
          ),
          elevation: 20,
          margin: const EdgeInsets.all(10),
          child: Column(
            children: [
              AspectRatio(
                aspectRatio: 16 / 9,
                child: Image.network(
                    "https://www.itying.com/images/flutter/3.png",
                    fit: BoxFit.cover),
              ),
              ListTile(
                leading: ClipOval(
                  child:Image.network(
                    "https://www.itying.com/images/flutter/3.png",
                    fit: BoxFit.cover,
                    height: 40,
                    width: 40,
                ),
                ),
                title: const Text("xxxxxxxxx"),
                subtitle: const Text("xxxxxxxxx"),
              )
            ],
          ),
        ),
        Card(
          shape: RoundedRectangleBorder(
            borderRadius:BorderRadius.circular(10)
          ),
          elevation: 20,
          margin: const EdgeInsets.all(10),
          child: Column(
            children: [
              AspectRatio(
                aspectRatio: 16 / 9,
                child: Image.network(
                    "https://www.itying.com/images/flutter/3.png",
                    fit: BoxFit.cover),
              ),
              const ListTile(
                leading: CircleAvatar(
                  backgroundImage: NetworkImage("https://www.itying.com/images/flutter/4.png"),
                ),
                title: Text("xxxxxxxxx"),
                subtitle: Text("xxxxxxxxx"),
              )
            ],
          ),
        )
      ],
    );
  }
}

在这里插入图片描述

LayoutBuilder

通过 LayoutBuilder,我们可以在布局过程中拿到父组件传递的约束信息,然后我们可以根据约束信息动态的构建不同的布局。

比如我们实现一个响应式的 Column 组件 ResponsiveColumn,它的功能是当当前可用的宽度小于 200 时,将子组件显示为一列,否则显示为两列。简单来实现一下:

class ResponsiveColumn extends StatelessWidget {
  const ResponsiveColumn({Key? key, required this.children}) : super(key: key);
  final List<Widget> children;
  
  Widget build(BuildContext context) {
    // 通过 LayoutBuilder 拿到父组件传递的约束,然后判断 maxWidth 是否小于200
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints) {
        if (constraints.maxWidth < 200) { // 最大宽度小于200,显示单列 
          return Column(children: children, mainAxisSize: MainAxisSize.min);
        } else { // 大于200,显示双列 
          var _children = <Widget>[];
          for (var i = 0; i < children.length; i += 2) {
            if (i + 1 < children.length) {
              _children.add(Row(
                children: [children[i], children[i + 1]],
                mainAxisSize: MainAxisSize.min,
              ));
            } else {
              _children.add(children[i]);
            }
          }
          return Column(children: _children, mainAxisSize: MainAxisSize.min);
        }
      },
    );
  }
}

class LayoutBuilderRoute extends StatelessWidget {
  const LayoutBuilderRoute({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    var _children = List.filled(6, Text("A"));
    // Column在本示例中在水平方向的最大宽度为屏幕的宽度
    return Column(
      children: [
        // 限制宽度为190,小于 200
        SizedBox(width: 190, child: ResponsiveColumn(children: _children)),
        ResponsiveColumn(children: _children),
        LayoutLogPrint(child:Text("xx")) // 下面介绍
      ],
    );
  }
}

可以发现 LayoutBuilder 的使用很简单,但是不要小看它,因为它非常实用且重要,它主要有两个使用场景:

  1. 可以使用 LayoutBuilder 来根据设备的尺寸来实现响应式布局。
  2. LayoutBuilder 可以帮我们高效排查问题。比如我们在遇到布局问题或者想调试组件树中某一个节点布局的约束时 LayoutBuilder 就很有用。

打印布局时的约束信息

为了便于排错,我们封装一个能打印父组件传递给子组件约束的组件:

class LayoutLogPrint<T> extends StatelessWidget {
  const LayoutLogPrint({
    Key? key,
    this.tag,
    required this.child,
  }) : super(key: key);

  final Widget child;
  final T? tag; //指定日志tag

  
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (_, constraints) {
      // assert在编译release版本时会被去除
      assert(() {
        print('${tag ?? key ?? child}: $constraints');
        return true;
      }());
      return child;
    });
  }
}

这样,我们就可以使用 LayoutLogPrint 组件树中任意位置的约束信息,比如:

LayoutLogPrint(child:Text("xx"))

控制台输出:

flutter: Text("xx"): BoxConstraints(0.0<=w<=428.0, 0.0<=h<=823.0)

可以看到 Text("xx") 的显示空间最大宽度为 428,最大高度为 823 。

注意!我们的大前提是盒模型布局,如果是Sliver 布局,可以使用 SliverLayoutBuiler 来打印。

运行效果:

在这里插入图片描述

Flutter 的 build 和 layout

通过观察 LayoutBuilder 的示例,我们还可以发现一个关于 Flutter 构建(build)和 布局(layout)的结论:Flutter 的 build 和 layout 是可以交错执行的,并不是严格的按照先 buildlayout 的顺序。比如在上例中 ,在build过程中遇到了 LayoutBuilder 组件,而 LayoutBuilderbuilder 是在 layout 阶段执行的(layout阶段才能取到布局过程的约束信息),在 builder 中新新建了一个 widget 后,Flutter 框架随后会调用该 widgetbuild 方法,又进入了 build 阶段。

AfterLayout

1. 获取组件大小和相对于屏幕的坐标

Flutter 是响应式UI框架,而命令式UI框架最大的不同就是:大多数情况下开发者只需要关注数据的变化,数据变化后框架会自动重新构建UI而不需要开发者手动去操作每一个组件,所以我们会发现 Widget 会被定义为不可变的(immutable),并且没有提供任何操作组件的 API,因此如果我们想在 Flutter 中获取某个组件的大小和位置就会很困难,当然大多数情况下不会有这个需求,但总有一些场景会需要,而在命令式UI框架中是不会存在这个问题的。

我们知道,只有当布局完成时,每个组件的大小和位置才能确定,所以获取的时机肯定是布局完成后,那布局完成的时机如何获取呢?至少事件分发肯定是在布局完成之后的,比如:

Builder(
  builder: (context) {
    return GestureDetector(
      child: Text('flutter'),
      onTap: () => print(context.size), //打印 text 的大小
    );
  },
),

context.size 可以获取当前上下文 RenderObject 的大小,对于BuilderStatelessWidget 以及 StatefulWidget 这样没有对应 RenderObject 的组件(这些组件只是用于组合和代理组件,本身并没有布局和绘制逻辑),获取的是子代中第一个拥有 RenderObject 组件的 RenderObject 对象。

虽然事件点击时可以拿到组件大小,但有两个问题,第一是需要用户手动触发,第二是时机较晚,更多的时候我们更希望在布局一结束就去获取大小和位置信息,为了解决这个问题,我们可以自己封装一个 AfterLayout 组件,它可以在子组件布局完成后执行一个回调,并同时将 RenderObject 对象作为参数传递。

以下是 AfterLayout 实现源码:

typedef AfterLayoutCallback = Function(RenderAfterLayout ral);

/// A widget can retrieve its render object after layout.
///
/// Sometimes we need to do something after the build phase is complete,
/// for example, most of [RenderObject] methods and attributes, such as
/// `renderObject.size`、`renderObject.localToGlobal(...)` only can be used
/// after build.
///
/// Call `setState` in callback is **allowed**, it is safe!
class AfterLayout extends SingleChildRenderObjectWidget {
  const AfterLayout({Key? key, required this.callback, Widget? child,}) : super(key: key, child: child);

  
  RenderObject createRenderObject(BuildContext context) {
    return RenderAfterLayout(callback);
  }

  
  void updateRenderObject(context, RenderAfterLayout renderObject) {
    renderObject.callback = callback;
  }

  /// 组件树布局结束后会被触发,注意,并不是当前组件布局结束后触发
  /// [callback] will be triggered after the layout phase ends.
  final AfterLayoutCallback callback;
}

class RenderAfterLayout extends RenderProxyBox {
  RenderAfterLayout(this.callback);

  ValueSetter<RenderAfterLayout> callback;

  
  void performLayout() {
    super.performLayout();
    // 不能直接回调callback,在 frame 结束的时候再去触发回调。 
    SchedulerBinding.instance.addPostFrameCallback((timeStamp) => callback(this));
  }

  /// 组件在在屏幕坐标中的起始偏移坐标
  Offset get offset => localToGlobal(Offset.zero);

  /// 组件在屏幕上占有的矩形空间区域
  Rect get rect => offset & size;
}

AfterLayout 可以在布局结束后拿到子组件的代理渲染对象 (RenderAfterLayout), RenderAfterLayout 对象会代理子组件渲染对象 ,因此,通过RenderAfterLayout 对象也就可以获取到子组件渲染对象上的属性,比如件大小、位置等。

上面代码有三点需要注意:

  1. callback 调用时机不是在子组件完成布局后就立即调用,原因是子组件布局完成后可能还有其他组件未完成布局,如果此时调用callback,一旦 callback 中存在触发更新的代码(比如调用了 setState)则会报错。因此我们在 frame 结束的时候再去触发回调。

  2. RenderAfterLayoutperformLayout方法中直接调用了父类 RenderProxyBoxperformLayout方法:

void performLayout() {
  if (child != null) {
    child!.layout(constraints, parentUsesSize: true);
    size = child!.size;
  } else {
    size = computeSizeForNoChild(constraints);
  }
}

可以看到是直接将父组件传给自身的约束传递给子组件,并将子组件的大小设置为自身大小。也就是说 RenderAfterLayout 的大小和其子组件大小是相同的

  1. 我们定义了 offsetrect 两个属性,它们是组件相对于屏幕的的位置偏移和占用的矩形空间范围。

AfterLayout 使用示例:

AfterLayout(
  callback: (RenderAfterLayout ral) {
    print(ral.size); //子组件的大小
    print(ral.offset); // 子组件在屏幕中坐标
  },
  child: Text('flutter'),
),

运行后控制台输出:

flutter: Size(105.0, 17.0)
flutter: Offset(42.5, 290.0)

可以看到 Text 文本的实际长度是 105,高度是 17,它的起始位置坐标是(42.5, 290.0)。

2. 获取组件相对于某个父组件的坐标

RenderAfterLayout 类继承自 RenderBoxRenderBox 有一个 localToGlobal 方法,它可以将坐标转化为相对与指定的祖先节点的坐标,比如下面代码可以打印出 Text('A') 在 父 Container 中的坐标:

Builder(builder: (context) {
  return Container(
    color: Colors.grey.shade200,
    alignment: Alignment.center,
    width: 100,
    height: 100,
    child: AfterLayout(
      callback: (RenderAfterLayout ral) {
        Offset offset = ral.localToGlobal(
          Offset.zero,
          // 传一个父元素 Container 对应的 RenderObject 对象
          ancestor: context.findRenderObject(),
        );
        print('A 在 Container 中占用的空间范围为:${offset & ral.size}');
      },
      child: Text('A'),
    ),
  );
}),

下面是一个 AfterLayout 的完整测试示例:

class AfterLayoutRoute extends StatefulWidget {
  const AfterLayoutRoute({Key? key}) : super(key: key);

  
  _AfterLayoutRouteState createState() => _AfterLayoutRouteState();
}

class _AfterLayoutRouteState extends State<AfterLayoutRoute> {
  String _text = 'flutter 实战 ';
  Size _size = Size.zero;

  
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Builder(
            builder: (context) {
              return GestureDetector(
                child: Text(
                  'Text1: 点我获取我的大小',
                  textAlign: TextAlign.center,
                  style: TextStyle(color: Colors.blue),
                ),
                onTap: () => print('Text1: ${context.size}'),
              );
            },
          ),
        ),
        AfterLayout(
          callback: (RenderAfterLayout ral) {
            print('Text2: ${ral.size}, ${ral.offset}');
          },
          child: Text('Text2:flutter@wendux'),
        ),
        Builder(builder: (context) {
          return Container(
            color: Colors.grey.shade200,
            alignment: Alignment.center,
            width: 100,
            height: 100,
            child: AfterLayout(
              callback: (RenderAfterLayout ral) {
                Offset offset = ral.localToGlobal(
                  Offset.zero,
                  ancestor: context.findRenderObject(),
                );
                print('A 在 Container 中占用的空间范围为:${offset & ral.size}');
              },
              child: Text('A'),
            ),
          );
        }),
        Divider(),
        AfterLayout(
          child: Text(_text), 
          callback: (RenderAfterLayout value) {
            setState(() {
              //更新尺寸信息
              _size = value.size;
            });
          },
        ),
        //显示上面 Text 的尺寸
        Padding(
          padding: const EdgeInsets.symmetric(vertical: 8.0),
          child: Text(
            'Text size: $_size ',
            style: TextStyle(color: Colors.blue),
          ),
        ),
        ElevatedButton(
          onPressed: () {
            setState(() {
              _text += 'flutter 实战 ';
            });
          },
          child: Text('追加字符串'),
        ),
      ],
    );
  }
}

运行效果:

在这里插入图片描述

运行后点击 Text1 就可以在日志面板看到它的大小。点击 “追加字符串” 按钮,字符串大小变化后,屏幕上上也会显示变化后的文本区域大小(按钮上方挨着)。


参考:《Flutter实战·第二版》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/566474.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

企业级WordPress开发 – 创建企业级网站的优秀提示

目录 “企业级”是什么意思&#xff1f; 使用WordPress创建企业级网站有什么好处&#xff1f; 使用 WordPress 进行企业开发的主要好处 WordPress 可扩展、灵活且价格合理 WordPress 提供响应式 Web 开发 WordPress 提供了巨大的可扩展性 不断更新使 WordPress 万无一…

JAVA-创建PDF文档

目录 一、前期准备 1、中文字体文件 2、maven依赖 二、创建PDF文档方法 三、通过可填充PDF模板将业务参数进行填充 1、 设置可填充的PDF表单 2、代码开干&#xff0c;代码填充可编辑PDF并另存文件 一、前期准备 1、中文字体文件 本演示使用的是iText 7版本&#xff0c…

Jeddak-DPSQL 首次开源!基于差分隐私的 SQL 代理保护能力

动手点关注 干货不迷路 ‍ ‍1. 背景 火山引擎对于用户敏感数据尤为重视&#xff0c;在火山引擎提供的数据分析产品中&#xff0c;广泛采用差分隐私技术对用户敏感信息进行保护。此类数据产品通常构建于 ClickHouse 等数据引擎之上&#xff0c;以 SQL 查询方式来执行计算逻辑&a…

【计算机网络复习】第六章 关系数据理论 1

关系模式的设计 按照一定的原则从数量众多而又相互关联的数据中&#xff0c;构造出一组既能较好地反映现实世界&#xff0c;而又有良好的操作性能的关系模式 ●冗余度高 ●修改困难 ●插入问题 ●删除问题 ★产生问题的原因 属性间约束关系&#xff08;即数据间的依赖关系&…

【JavaSE】Java基础语法(十):构造方法

文章目录 ⛄1. 构造方法的格式和执行时机⛄2. 构造方法的作用⛄3. 构造方法的特点⛄4. 构造方法的注意事项⛄5. 构造方法为什么不能被重写 在面向对象编程的思想中&#xff0c;构造方法&#xff08;Constructor&#xff09;是一个特殊的函数&#xff0c;用于创建和初始化类的对…

“数字”厨电成新宠?“传统”厨电如何凭实力年销破百亿?|厨房电器SMI社媒心智品牌榜

Social Power 核心解读 AI加持&#xff0c;数字厨电新物种持续走红 传统厨电发力社媒&#xff0c;“有范儿”实力吸睛 4月中下旬的“魔都”可谓热闹非凡&#xff0c;上海车展喧嚣未落&#xff0c;隔壁2023AWE&#xff08;中国家电及消费电子博览会&#xff09;的群雄逐鹿紧随…

Electron 小白介绍,你能看懂吗?

目录 前言一、Electron是什么1.官网介绍2.小白介绍 二、Electron开发了哪些应用三、Electron的优势在哪里1.优势2.带给我们什么优势 四、Electron如何学习1.前置知识2.学习建议 五、Electron的乐趣总结 前言 在最近的学习中&#xff0c;我接触了 Electron 这个前端框架&#x…

总结加载Shellcode的各种方式(更新中!)

1.内联汇编加载 使用内联汇编只能加载32位程序的ShellCode&#xff0c;因为64位程序不支持写内联汇编 #pragma comment(linker, "/section:.data,RWE") //将data段的内存设置成可读可写可执行 #include <Windows.h>//ShellCode部分 unsigned char buf[] &qu…

Hadoop的基础操作

Hadoop的基础操作 HDFS是Hadoop的分布式文件框架&#xff0c;它的实际目标是能够在普通的硬件上运行&#xff0c;并且能够处理大量的数据。HDFS采用主从架构&#xff0c;其中由一个NameNode和多个DataNode NameNode负责管理文件系统的命名空间和客户端的访问DataNode负责存储实…

企业为什么要做数字化转型,应该如何进行转型?

企业需要数字化转型才能在当今快速发展的商业环境中保持竞争力和相关性。数字化转型涉及利用数字技术和战略从根本上改变企业的运营方式、为客户创造价值并实现他们的目标。以下是企业进行数字化转型的一些关键原因&#xff1a; 提高运营效率&#xff1a;数字技术可实现自动化、…

如何使用ArcGIS标注上下标

&#xff08;本文首发于“水经注GIS”公号&#xff0c;关注公号免费领取地图数据&#xff09; 在某些情况下除了需要普通的标注之外还需要上下标注&#xff0c;对于这一需求&#xff0c;ArcGIS是支持的&#xff0c;这里为大家介绍一下ArcGIS标注上下标的方法&#xff0c;希望能…

初阶数据结构之栈的实现(五)

文章目录 &#x1f60f;专栏导读&#x1f916;文章导读&#x1f640;什么是栈&#xff1f;&#x1f640;画图描述 &#x1f633;栈的代码实现及其各类讲解&#x1f633;栈的初始化代码实现及其讲解&#x1f633;栈的初始化 &#x1f633;栈的销毁代码实现及其讲解&#x1f633;…

【面试题】2023vue面试题

大厂面试题分享 面试题库 前后端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 web前端面试题库 VS java后端面试题库大全 1、说说你对 SPA 单页面的理解&#xff0c;它的优缺点分别是什么&#xff1f; SPA&#xf…

【运维知识进阶篇】集群架构-Nginx高可用Keepalived

高可用是指2台机器启动着完全相同的业务系统&#xff0c;一台机器宕机后&#xff0c;另一台可以快速启用&#xff0c;用户是无感知的。高可用硬件通常使用F5&#xff0c;软件通常使用keepalived。keepalived软件是基于VRRP协议实现的&#xff0c;VRRP虚拟路由冗余协议&#xff…

详解Node.js开发中不可或缺的7个库

在Node.js开发中&#xff0c;选择合适的库对于提高开发效率和优化应用程序性能至关重要。本文将介绍七个备受关注的Node.js库&#xff0c;它们在各自的领域中展现了出色的功能和性能。这些库分别是&#xff1a;Config、Fetch、Ioredis、Multer、Cache、Fast-xml-parser和Cron。…

一图看懂 pkg_resources 模块:包资源API,资料整理+笔记(大全)

本文由 大侠(AhcaoZhu)原创&#xff0c;转载请声明。 链接: https://blog.csdn.net/Ahcao2008 一图看懂 pkg_resources 模块&#xff1a;包资源API&#xff0c;资料整理笔记&#xff08;大全&#xff09; &#x1f9ca;摘要&#x1f9ca;模块图&#x1f9ca;类关系图&#x1f9…

JavaEE(系列15) -- 多线程(JUC中常见的类)

JUC----- java.util.concurrent(并发) 1. ReentrantLock 1. 可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全. 2. ReentrantLock 也是可重入锁. "Reentrant" 这个单词的原意就是 "可重入". 1. ReentrantLock 的用法: lock():…

【花雕学AI】微软 Bing 图像魔法师:让你的描述变成图像,让你的图像变成现实

你有没有想过&#xff0c;如果你能够用语言来创造图像&#xff0c;那该有多么神奇和有趣&#xff1f;你有没有想过&#xff0c;如果你能够看到你想象中的图像&#xff0c;那该有多么震撼和美妙&#xff1f;现在&#xff0c;这一切都可以实现了&#xff0c;因为微软 Bing 图像魔…

NetApp EF 系列全闪存阵列——性能极佳,性价比优势突出

NetApp EF 系列全闪存阵列——性能极佳&#xff0c;性价比优势突出 如果您需要为实时分析、HPC 和数据库等性能敏感型工作负载提供强劲动力&#xff0c;NetApp EF 系列全闪存阵列的性价比优势不言自明。其可为要求最苛刻的应用程序提供微秒级响应&#xff0c;最大限度地延长正…

电源方案对比

电源 1.方案选择&#xff1a;1 LM2596 2 MP1584 3&#xff1a;TPS54301LM25962.MP1584&#xff1a;3.TPS5430 2.1输出2A电流的纹波2.2 输出3A电流的纹波3.动态响应4.发热5.电源转换效率6.综合指标reference 1.方案选择&#xff1a;1 LM2596 2 MP1584 3&#xff1a;TPS5430 1LM…