不听话的 Container

朱异
• 阅读 5457

不听话的 Container
不听话的 Container

前言

在阅读本文之前我们先来回顾下在 Flutter 开发过程中,是不是经常会遇到以下问题:

  • Container 设置了宽高无效
  • Column 溢出边界,Row 溢出边界
  • 什么时候该使用 ConstrainedBox 和 UnconstrainedBox

每当遇到这种问题,我总是不断地尝试,费了九牛二虎之力,Widget 终于乖乖就范(达到理想效果)。痛定思过,我终于开始反抗(起来,不愿做奴隶的人们,国歌唱起来~),为什么 Container 设置宽高又无效了?Column 为什么又溢出边界了?怀揣着满腔热血,我终于鼓起勇气首先从 Container 源码入手,逐一揭开它的神秘面纱。

布局规则

在讲本文之前,我们首先应该了解 Flutter 布局中的以下规则:

  • 首先,上层 Widget 向下层 Widget 传递约束条件
  • 其次,下层 Widget 向上层 Widget 传递大小信息
  • 最后,上层 Widget 决定下层 Widget 的位置

如果我们在开发时无法熟练运用这些规则,在布局时就不能完全理解其原理,所以越早掌握这些规则越好。

  • Widget 会通过它的父级获得自身约束。约束实际上就是 4 个浮点类型的集合:最大/最小宽度,以及最大/最小高度。
  • 然后这个 Widget 将会逐个遍历它的 children 列表,向子级传递约束(子级之间的约束可能会有不同),然后询问它的每一个子级需要用于布局的大小。
  • 然后这个 Widget 将会对它子级 children 逐个进行布局。
  • 最后,Widget 将会把它的大小信息向上传递至父 Widget(包括其原始约束条件)。

严格约束(Tight)vs. 宽松约束(Loose)

严格约束就是获得确切大小的选择,换句话来说,它的最大/最小宽度是一致的,高度也是一样。

// flutter/lib/src/rendering/box.dart
BoxConstraints.tight(Size size)
    : minWidth = size.width,
      maxWidth = size.width,
      minHeight = size.height,
      maxHeight = size.height;

宽松约束就是设置了最大宽度/高度,但是允许其子 Widget 获得比它更小的任意大小,换句话说就是宽松约束的最小宽度/高度为 0。

// flutter/lib/src/rendering/box.dart
BoxConstraints.loose(Size size)
    : minWidth = 0.0,
      maxWidth = size.width,
      minHeight = 0.0,
      maxHeight = size.height;

Container 部分源码

首先奉上 Container 部分源码,下面我们会结合具体场景对源码进行逐一分析。

// flutter/lib/src/widgets/container.dart
class Container extends StatelessWidget {
  Container({
    Key key,
    this.alignment,
    this.padding,
    this.color,
    this.decoration,
    this.foregroundDecoration,
    double width,
    double height,
    BoxConstraints constraints,
    this.margin,
    this.transform,
    this.child,
    this.clipBehavior = Clip.none,
  })  : assert(margin == null || margin.isNonNegative),
        assert(padding == null || padding.isNonNegative),
        assert(decoration == null || decoration.debugAssertIsValid()),
        assert(constraints == null || constraints.debugAssertIsValid()),
        assert(clipBehavior != null),
        assert(
            color == null || decoration == null,
            'Cannot provide both a color and a decoration\n'
            'To provide both, use "decoration: BoxDecoration(color: color)".'),
        constraints = (width != null || height != null)
            ? constraints?.tighten(width: width, height: height) ??
                BoxConstraints.tightFor(width: width, height: height)
            : constraints,
        super(key: key);

  final Widget child;

  // child 元素在 Container 中的对齐方式
  final AlignmentGeometry alignment;

  // 填充内边距
  final EdgeInsetsGeometry padding;

  // 颜色
  final Color color;

  // 背景装饰
  final Decoration decoration;

  // 前景装饰
  final Decoration foregroundDecoration;

  // 布局约束
  final BoxConstraints constraints;

  // 外边距
  final EdgeInsetsGeometry margin;

  // 绘制容器之前要应用的变换矩阵
  final Matrix4 transform;

  // decoration 参数具有 clipPath 时的剪辑行为
  final Clip clipBehavior;

  EdgeInsetsGeometry get _paddingIncludingDecoration {
    if (decoration == null || decoration.padding == null) return padding;
    final EdgeInsetsGeometry decorationPadding = decoration.padding;
    if (padding == null) return decorationPadding;
    return padding.add(decorationPadding);
  }

  @override
  Widget build(BuildContext context) {
    Widget current = child;

    if (child == null && (constraints == null || !constraints.isTight)) {
      current = LimitedBox(
        maxWidth: 0.0,
        maxHeight: 0.0,
        child: ConstrainedBox(constraints: const BoxConstraints.expand()),
      );
    }

    if (alignment != null)
      current = Align(alignment: alignment, child: current);

    final EdgeInsetsGeometry effectivePadding = _paddingIncludingDecoration;
    if (effectivePadding != null)
      current = Padding(padding: effectivePadding, child: current);

    if (color != null) current = ColoredBox(color: color, child: current);

    if (decoration != null)
      current = DecoratedBox(decoration: decoration, child: current);

    if (foregroundDecoration != null) {
      current = DecoratedBox(
        decoration: foregroundDecoration,
        position: DecorationPosition.foreground,
        child: current,
      );
    }

    if (constraints != null)
      current = ConstrainedBox(constraints: constraints, child: current);

    if (margin != null) current = Padding(padding: margin, child: current);

    if (transform != null)
      current = Transform(transform: transform, child: current);

    if (clipBehavior != Clip.none) {
      current = ClipPath(
        clipper: _DecorationClipper(
            textDirection: Directionality.of(context), decoration: decoration),
        clipBehavior: clipBehavior,
        child: current,
      );
    }

    return current;
  }
}

场景分析

场景一

Scaffold(
  appBar: AppBar(
    title: Text('Flutter Container'),
  ),
  body: Container(
    color: Colors.red,
  ),
),

在 Scaffold body 中单独使用 Container,并且 Container 设置 color 为 Colors.red。

打开 DevTools 进行元素检查我们可以发现 Widget Tree 的结构 Container -> ColoredBox -> LimitedBox -> ConstrainedBox,最后会创建 RenderConstrainedBox,宽度和高度撑满整个屏幕(除了 AppBar)。

不听话的 Container

那我们不禁会问,为什么会这样,我并没有设置 Container 的宽度和高度,那么我们再次回到上面的源码,如果 Container 没有设置 child 参数并且满足 constraints == null || !constraints.isTight 会返回一个 maxWidth 为 0,maxHeight 为 0 的 LimitedBox 的元素,并且 LimitedBox 的 child 是一个 constraints 参数为 const BoxConstraints.expand() 的 ConstrainedBox 的元素,所以 Container 会撑满整个屏幕(除了 AppBar)。

// flutter/lib/src/widgets/container.dart

if (child == null && (constraints == null || !constraints.isTight)) {
    current = LimitedBox(
      maxWidth: 0.0,
      maxHeight: 0.0,
      child: ConstrainedBox(constraints: const BoxConstraints.expand()),
    );
  }
// flutter/lib/src/rendering/box.dart
const BoxConstraints.expand({
    double width,
    double height,
  }) : minWidth = width ?? double.infinity,
       maxWidth = width ?? double.infinity,
       minHeight = height ?? double.infinity,
       maxHeight = height ?? double.infinity;

场景二

Scaffold(
    appBar: AppBar(
      title: Text('Flutter Container'),
    ),
    body: Container(
      width: 100,
      height: 100,
      color: Colors.red,
    ),
  ),

在场景一的基础上进行修改,此时给 Container 设置 width 为 100,height 为 100,color 为 Colors.red。

同样打开 DevTools 进行元素检查我们可以发现 Widget Tree 的结构 Container -> ConstrainedBox -> ColorededBox,最后会创建 _RenderColoredBox,宽度和高度均为 100,颜色为红色的正方形。

不听话的 Container

通过源码分析我们可以得出,如果 Container 中设置了 width、height 并且没有设置 constraints 属性,首先会在构造函数中对 constraints 进行赋值,所以 constraints = BoxConstraints.tightFor(width:100, height:100),然后会在外层嵌套一个 ColoredBox,最后再嵌套一个 ConstrainedBox 返回。

Container({
    Key key,
    this.alignment,
    this.padding,
    this.color,
    this.decoration,
    this.foregroundDecoration,
    double width,
    double height,
    BoxConstraints constraints,
    this.margin,
    this.transform,
    this.child,
    this.clipBehavior = Clip.none,
  }) : assert(margin == null || margin.isNonNegative),
       assert(padding == null || padding.isNonNegative),
       assert(decoration == null || decoration.debugAssertIsValid()),
       assert(constraints == null || constraints.debugAssertIsValid()),
       assert(clipBehavior != null),
       assert(color == null || decoration == null,
         'Cannot provide both a color and a decoration\n'
         'To provide both, use "decoration: BoxDecoration(color: color)".'
       ),
       constraints =
        (width != null || height != null)
          ? constraints?.tighten(width: width, height: height)
            ?? BoxConstraints.tightFor(width: width, height: height)
          : constraints,
       super(key: key);
if (decoration != null)
      current = DecoratedBox(decoration: decoration, child: current);

if (constraints != null)
      current = ConstrainedBox(constraints: constraints, child: current);

场景三

Scaffold(
    appBar: AppBar(
      title: Text('Flutter Container'),
    ),
    body: Container(
      width: 100,
      height: 100,
      color: Colors.red,
      alignment: Alignment.center,
    ),
  ),

接下来,我们在场景二的基础上继续添加 alignment:Alignment.center 属性。

不听话的 Container

此时我们会发现为什么没有居中显示呢?通过查看 Align 源码不难发现,它是设置子 Widget 与自身的对齐方式。

A widget that aligns its child within itself and optionally sizes itself based on the child's size.

那么此时我们再来改变代码,给当前 Container 添加子 Widget,终于达到了我们想要的居中效果。

Scaffold(
    appBar: AppBar(
      title: Text('Flutter Container'),
    ),
    body: Container(
      width: 100,
      height: 100,
      color: Colors.red,
      alignment: Alignment.center,
      child: Container(
        width: 10,
        height: 10,
        color: Colors.blue,
      ),
    ),
  ),

不听话的 Container

场景四

Scaffold(
    appBar: AppBar(
      title: Text('Flutter Container'),
    ),
    body: Center(
      child: Container(
        color: Colors.red,
        width: 200,
      ),
    ),
  ),

不听话的 Container

由于 Scaffold 中的 body 元素会撑满整个屏幕(除了 AppBar),body 告诉 Center 占满整个屏幕,然后 Center 告诉 Container 可以变成任意大小,但是 Container 设置 width 为 200,所以 Container 的大小为宽度 200, 高度无限大。

The primary content of the scaffold.
Displayed below the [appBar], above the bottom of the ambient

场景五

Scaffold(
    appBar: AppBar(
      title: Text('Flutter Container'),
    ),
    body: Center(
      child: Row(
        children: <Widget>[
          Container(
            color: Colors.red,
            child: Text(
              '我是一段很长很长很长的文字',
              style: TextStyle(
                fontSize: 30,
              ),
            ),
          ),
          Container(
            color: Colors.red,
            child: Text(
              '我是一段很短的文字',
            ),
          ),
        ],
      ),
    ),
  ),

不听话的 Container

由于 Row 不会对其子元素施加任何约束,因此它的 children 很有可能太大而超出 Row 的宽度,在这种情况下,Row 就会显示出溢出警告了。

场景六

Scaffold(
    appBar: AppBar(
      title: Text('Flutter Container'),
    ),
    body: Center(
      child: Container(
        constraints: BoxConstraints(
          maxHeight: 400,
          minHeight: 300,
          minWidth: 300,
          maxWidth: 400,
        ),
        color: Colors.red,
        width: 200,
      ),
    ),
  ),

不听话的 Container

这里我们设置了 Container 的 constraints 属性值为 BoxConstraints(minHeight:300, maxHeight:400, minWidth:300, maxWidth:400), 并且设置了 width 为 200。所以在构造函数初始化参数时,会进行设置 constraints = BoxConstraints(minHeight:300, maxHeight:400, minWidth:300, maxWidth:300) , 在 Container build 函数中会返回一个这样的 Widget Tree 的结构(Container -> ConstrainedBox -> ColoredBox -> LimitedBox -> ConstrainedBox)。

此时 Center 告诉 Container 可以变成任意大小,但是 Container 设置 constraints 约束条件为宽度最小为 300,最大为 300,也就是宽度为 300, 最小高度为 300, 最大高度为 400,所以在 Container 中设置的 width 为 200 也就无效了,这个时候你也许会问,那高度到底是多少?答案是 400,因为 Container 中没有设置 child ,满足 child == null && (constraints == null || !constraints.isTight) 条件,所以会嵌套一个 ConstrainedBox(constraints: const BoxConstraints.expand() 所以高度会为最大高度 400。

// flutter/lib/src/rendering/box.dart
BoxConstraints tighten({ double width, double height }) {
  return BoxConstraints(
    minWidth: width == null ? minWidth : width.clamp(minWidth, maxWidth) as double,
    maxWidth: width == null ? maxWidth : width.clamp(minWidth, maxWidth) as double,
    minHeight: height == null ? minHeight : height.clamp(minHeight, maxHeight) as double,
    maxHeight: height == null ? maxHeight : height.clamp(minHeight, maxHeight) as double,
  );
}
// flutter/lib/src/rendering/box.dart
/// Whether there is exactly one width value that satisfies the constraints.
bool get hasTightWidth => minWidth >= maxWidth;

/// Whether there is exactly one height value that satisfies the constraints.
bool get hasTightHeight => minHeight >= maxHeight;

/// Whether there is exactly one size that satisfies the constraints.
@override
bool get isTight => hasTightWidth && hasTightHeight;
// flutter/lib/src/widgets/container.dart
if (child == null && (constraints == null || !constraints.isTight)) {
    current = LimitedBox(
      maxWidth: 0.0,
      maxHeight: 0.0,
      child: ConstrainedBox(constraints: const BoxConstraints.expand()),
    );
  }

不听话的 Container

最后

通过以上源码分析以及不同的场景,我们不难发现 Container 主要就是通过设置不同的参数,然后使用 LimitedBox、ConstrainedBox、Align、Padding、ColoredBox、DecoratedBox、Transform、ClipPath 等 Widget 进行组合而来。

更多精彩请关注我们的公众号「百瓶技术」,有不定期福利呦!

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Stella981 Stella981
3年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Easter79 Easter79
3年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
朱异
朱异
Lv1
楚天阔,浪浸斜阳,千里溶溶。
文章
2
粉丝
0
获赞
0