flutter布局约束原理深入解析

目录
  • 引言
    • 1、flutter的widget类型
    • 2、Container是个组合类
    • 3、flutter布局约束
    • 4、Container布局行为解惑
  • 总结

引言

刚开始接触flutter的时候,Container组件是用得最多的。它就像HTML中的div一样普遍,专门用来布局页面的。

但是使用Container嵌套布局的时候,经常出现一些令人无法理解的问题。就如下面代码,在一个固定的容器中,子组件却铺满了全屏。

/// 例一
@override
Widget build(BuildContext context) {
   return Container(
     width: 300,
     height: 300,
     color: Colors.amber,
     child: Container(width: 50, height: 50, color: Colors.red,),
  );
}

然后要加上alignment属性,子组件正常显示了,但容器还是铺满全屏。

/// 例二
@override
Widget build(BuildContext context) {
   return Container(
     width: 300,
     height: 300,
     color: Colors.amber,
     alignment: Alignment.center,
     child: Container(width: 50, height: 50, color: Colors.red,),
  );
}

而在容器外层添加一个Scaffold组件,它就正常显示了。

/// 例三
@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Container(
      width: 300,
      height: 300,
      color: Colors.amber,
      alignment: Alignment.center,
      child: Container(width: 50, height: 50, color: Colors.red,),
    ),
  );
}

这一切的怪异行为困扰了我很久,直到我深入了flutter布局的学习,才渐渐解开这些疑惑。

1、flutter的widget类型

flutter的widget可以分为三类,组合类ComponentWidget代理类ProxyWidget绘制类RenderObjectWidget

组合类:如ContainerScaffoldMaterialApp还有一系列通过继承StatelessWidgetStatefulWidget的类。组合类是我们开发过程中用得最多的组件。

代理类InheritedWidget,功能型组件,它可以高效快捷的实现共享数据的跨组件传递。如常见的ThemeMediaQuery就是InheritedWidget的应用。

绘制类:屏幕上看到的UI几乎都会通过RenderObjectWidget实现。通过继承它,可以进行界面的布局和绘制。如AlignPaddingConstrainedBox等都是通过继承RenderObjectWidget,并通过重写createRenderObject方法来创建RenderObject对象,实现最终的布局(layout)和绘制(paint)。

2、Container是个组合类

显而易见Container继承StatelessWidget,它是一个组合类,同时也是一个由DecoratedBoxConstrainedBoxTransformPaddingAlign等组件组合的多功能容器。可以通过查看Container类,看出它实际就是通过不同的参数判断,再进行组件的层层嵌套来实现的。

@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()),
    );
  } else 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 (clipBehavior != Clip.none) {
    assert(decoration != null);
    current = ClipPath(
      clipper: _DecorationClipper(
        textDirection: Directionality.maybeOf(context),
        decoration: decoration!,
      ),
      clipBehavior: clipBehavior,
      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!, alignment: transformAlignment, child: current);
  }
  return current!;
}

组合类基本不参与ui的绘制,都是通过绘制类的组合来实现功能。

3、flutter布局约束

flutter中有两种布局约束BoxConstraints盒约束和SliverConstraints线性约束,如Align、Padding、ConstrainedBox使用的是盒约束。

BoxConstraints盒约束是指flutter框架在运行时遍历整个组件树,在这过程中 「向下传递约束,向上传递尺寸」,以此来确定每个组件的尺寸和大小。

BoxConstraints类由4个属性组成,最小宽度minWidth、最大宽度maxWidth、最小高度minHeight、最大高度maxHeight

BoxConstraints({
    this.minWidth,
    this.maxWidth,
    this.minHeight,
    this.maxHeight,
});

根据这4个属性的变化,可以分为“紧约束(tight)”、“松约束(loose)”、“无界约束”、“有界约束”。

紧约束:最小宽(高)度和最大宽(高)度值相等,此时它是一个固定宽高的约束。

BoxConstraints.tight(Size size)
    : minWidth = size.width,
      maxWidth = size.width,
      minHeight = size.height,
      maxHeight = size.height;

松约束:最小宽(高)值为0,最大宽(高)大于0,此时它是一个约束范围。

BoxConstraints.loose(Size size)
    : minWidth = 0.0,
      maxWidth = size.width,
      minHeight = 0.0,
      maxHeight = size.height;

无界约束:最小宽(高)和最大宽(高)值存在double.infinity(无限)。

BoxConstraints.expand({double? width, double? height})
: minWidth = width ?? double.infinity,
     maxWidth = width ?? double.infinity,
     minHeight = height ?? double.infinity,
     maxHeight = height ?? double.infinity;

有界约束:最小宽(高)和最大宽(高)值均为固定值。

BoxConstraints(100, 300, 100, 300)

4、Container布局行为解惑

了解了BoxConstraints布局约束,回到本文最开始的问题。

/// 例一
@override
Widget build(BuildContext context) {
   return Container(
     width: 300,
     height: 300,
     color: Colors.amber,
     child: Container(width: 50, height: 50, color: Colors.red,),
  );
}

例一中,两个固定宽高的Container,为什么子容器铺满了全屏?

根据BoxConstraints布局约束,遍历整个组件树,最开始的root是树的起点,它向下传递的是一个紧约束。因为是移动设备,root即是屏幕的大小,假设屏幕宽414、高896。于是整个布局约束如下:

这里有个问题,就是Container分明已经设置了固定宽高,为什么无效?

因为父级向下传递的约束,子组件必须严格遵守。这里Container容器设置的宽高超出了父级的约束范围,就会自动被忽略,采用符合约束的值。

例一两上Container都被铺满屏幕,而最底下的红色Container叠到了最上层,所以最终显示红色。

/// 例二
@override
Widget build(BuildContext context) {
   return Container(
     width: 300,
     height: 300,
     color: Colors.amber,
     alignment: Alignment.center,
     child: Container(width: 50, height: 50, color: Colors.red,),
  );
}

例二也同样可以根据布局约束求证,如下图:

这里Container为什么是ConstrainedBoxAlign组件?前面说过Container是一个组合组件,它是由多个原子组件组成的。根据例二,它是由ConstrainedBox和Align嵌套而成。

Align提供给子组件的是一个松约束,所以容器自身设置50宽高值是在合理范围的,因此生效,屏幕上显示的就是50像素的红色方块。ConstrainedBox受到的是紧约束,所以自身的300宽高被忽略,显示的是铺满屏幕的黄色块。

/// 例三
@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Container(
      width: 300,
      height: 300,
      color: Colors.amber,
      alignment: Alignment.center,
      child: Container(width: 50, height: 50, color: Colors.red,),
    ),
  );
}

例三中Scaffold向下传递的是一个松约束,所以黄色Container的宽高根据自身设置的300,在合理的范围内,有效。Container再向下传递的也是松约束,最终红色Container宽高为50。

这里还有个问题,怎么确定组件向下传递的是紧约束还是松约束?

这就涉及到组件的内部实现了,这里通过Align举个例。

Align是一个绘制组件,它能够进行界面的布局和绘制,这是因为Align的继承链为:

Align -> SingleChildRenderObjectWidget -> RenderObjectWidget

Align需要重写createRenderObject方法,返回RenderObject的实现,这里Align返回的是RenderPositionedBox,所以核心内容就在这个类中

class Align extends SingleChildRenderObjectWidget {
    /// ...
    @override
    RenderPositionedBox createRenderObject(BuildContext context) {
      return RenderPositionedBox(
        alignment: alignment,
        widthFactor: widthFactor,
        heightFactor: heightFactor,
        textDirection: Directionality.maybeOf(context),
      );
    }
    /// ...
}

而RenderPositionedBox类中,重写performLayout方法,该方法用于根据自身约束条件,计算出子组件的布局,再根据子组件的尺寸设置自身的尺寸,形成一个至下而上,由上到下的闭环,最终实现界面的整个绘制。

RenderPositionedBox -> RenderAligningShiftedBox -> RenderShiftedBox -> RenderBox

class RenderPositionedBox extends RenderAligningShiftedBox {
    /// ...
    @override
    void performLayout() {
      final BoxConstraints constraints = this.constraints; // 自身的约束大小
      final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
      final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;
      /// 存在子组件
      if (child != null) {
        /// 开始布局子组件
        child!.layout(constraints.loosen(), parentUsesSize: true);
        /// 根据子组件的尺寸设置自身尺寸
        size = constraints.constrain(Size(
          shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
          shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity,
        ));
        /// 计算子组件的位置
        alignChild();
      } else {
        /// 不存在子组件
        size = constraints.constrain(Size(
          shrinkWrapWidth ? 0.0 : double.infinity,
          shrinkWrapHeight ? 0.0 : double.infinity,
        ));
      }
    }
    /// ...
}

根据Align中performLayout方法的实现,可以确定该组件最终会给子组件传递一个怎么样的约束。

/// constraints.loosen提供的是一个松约束
child!.layout(constraints.loosen(), parentUsesSize: true);
/// loosen方法
BoxConstraints loosen() {
  assert(debugAssertIsValid());
  /// BoxConstraints({double minWidth = 0.0, double maxWidth = double.infinity, double minHeight = 0.0, double maxHeight = double.infinity})
  return BoxConstraints(
    maxWidth: maxWidth,
    maxHeight: maxHeight,
  );
}

其它绘制类的组件基本跟Align大同小异,只要重点看performLayout方法的实现,即可判断出组件提供的约束条件。

总结

1、flutter的widget分为,组合类、代理类和绘制类。

2、Container是一个组合类,由DecoratedBox、ConstrainedBox、Transform、Padding、Align等绘制组件组合而成。

3、flutter中有两种布局约束BoxConstraints盒约束和SliverConstraints线性约束。

4、BoxConstraints的约束原理是: 「向下传递约束,向上传递尺寸」

5、BoxConstraints的约束类型为:紧约束、松约束、无界约束、有界约束。

6、判断一个绘制组件的约束行为可以通过查看performLayout方法中layout传入的约束值。

以上就是flutter布局约束原理深入解析的详细内容,更多关于flutter布局约束原理的资料请关注我们其它相关文章!

(0)

相关推荐

  • Flutter Widget移动UI框架使用Material和密匙Key实战

    目录 Flutter Material 更完整的示例 Key Flutter Flutter是谷歌的移动UI框架,可以在IOS和Android上快速构建高质量的本地用户界面.Flutter可以使用现有代码. 在世界上,Flutter正被越来越多的开发人员和组织使用,Flutter是完全免费和开源的.这也是构建未来Google Fuchsia应用程序的主要方式. import 'package:flutter/material.dart'; void main() { runApp( new Ce

  • 详解Flutter中key的正确使用方式

    目录 1.什么是key 2.key的更新原理 3.key的分类 GlobalKey LocalKey 总结 1.什么是key Widget中有个可选属性key,顾名思义,它是组件的标识符,当设置了key,组件更新时会根据新老组件的key是否相等来进行更新,可以提高更新效率.但一般我们不会去设置它,除非对某些具备状态且相同的组件进行添加.移除.或者排序时,就需要使用到key,不然就会出现一些莫名奇妙的问题. 例如下面的demo: import 'dart:math'; import 'packag

  • Flutter开发技巧RadialGradient中radius计算详解

    目录 一.问题来源 二.四种情况 1.情况一 2.情况二 3.情况三 4.情况四 三.实现源码 四.radiusOfRadialGradient 方法实现 最后 一.问题来源 项目中遇到 json 模型映射成 RadialGradient 组件的需求,其他参数正常传递即可: 唯独 radius 参数效果有出入,总结记录一下: 二.四种情况 通过 RadialGradient 参数 center 可以分为四种情况,这四种情况分别对应四种 radius 的值: 1.情况一 Alignment.cen

  • 替换so文件来动态替换Flutter代码实现详解

    目录 一.Flutter代码的启动起点 1.1 initTask对象 1.2 ResourceExtractor 1.3 FlutterJNI#loadLibrary 二.ensureInitializationComplete 2.1 ShellArgs 三.实践:自定义libapp.so的加载 3.1 flutterApplicationInfo和FlutterActivity#getShellArgs() 一.Flutter代码的启动起点 我们在多数的业务场景下,使用的都是FlutterA

  • Flutter Zone异常处理方法及基本原理

    目录 1. 认识Zone 1.1 ZoneValues 1.2 ZoneSpecification 1.3 通过runZoned快速创建Zone 2. 异步基本原理和异常捕获 3. HandleUncaughtErrorHandler 1. 认识Zone Zone像一个沙盒,是我们代码执行的一个环境. 我们的main函数默认就运行在Root Zone当中. 子Zone的构造有点像Linux中的进程,它支持从当前的Zone中Fork出一个子Zone: Zone myZone = Zone.curr

  • Flutter有状态组件StatefulWidget生命周期详解

    目录 1.StatefulWidget的背后 2.StatefulWidget的生命周期 2.1创建阶段 2.2更新阶段 2.3销毁阶段 总结: 1.StatefulWidget的背后 flutter开发过程中,我们经常会用到两个组件StatelessWidget和StatefulWidget.前者为无状组件,后者为有状态组件,无状态组件通常在创建后内部的数据无法改变,而有状态组件可以维持内部的数据状态,适合动态组件使用. /// 无状态组件的定义 class MyApp extends Sta

  • Android flutter Dio锁的巧妙实现方法示例

    正文 看Dio库源码的时候,发现其拦截器管理的逻辑处用到了一个Lock,这个Lock巧妙地利用了Completer和Future的机制来实现,记录一下. /// Add lock/unlock API for interceptors. class Lock { Future? _lock; late Completer _completer; /// 标识拦截器是否被上锁 bool get locked => _lock != null; /// Lock the interceptor. /

  • flutter开发技巧自定页面指示器PageIndicator详解

    目录 一.来源 二.效果 三.源码实现 1.flutter_swiper_null_safety 使用示例: 2.PageIndicatorWidget 指示器源码: 三.总结 一.来源 项目中遇到多个需要自定义轮播图指示器的需求,封装成基础组件方便复用: 原理是通过 ValueListenableBuilder 实时监听轮播图的当前索引,然后更新指示器组件,达到最终效果: 二.效果 三.源码实现 1.flutter_swiper_null_safety 使用示例: import 'packag

  • flutter布局约束原理深入解析

    目录 引言 1.flutter的widget类型 2.Container是个组合类 3.flutter布局约束 4.Container布局行为解惑 总结 引言 刚开始接触flutter的时候,Container组件是用得最多的.它就像HTML中的div一样普遍,专门用来布局页面的. 但是使用Container嵌套布局的时候,经常出现一些令人无法理解的问题.就如下面代码,在一个固定的容器中,子组件却铺满了全屏. /// 例一 @override Widget build(BuildContext

  • flutter Bloc 实现原理示例解析

    目录 序言 1. 事件流 > 状态流 (中转) 2. 使用 BlocBuilder 实时监听状态变更, 如何实现的呢? 总结 扩展 序言 在flutter开发中,我们使用 bloc 框架,基于状态变更进行响应式开发.本篇文章,小轰将 bloc 核心业务块进行拆解简化,聊一聊它的实现思想,bloc 核心能力分为如下两点: 添加事件 event,将 '事件流' 转换为 '状态流' state 监听 bloc 流,每次 state 状态变更,通知 widget 更新 下面,用自定义Bloc的方式,来给

  • react fiber执行原理示例解析

    目录 为什么要使用fiber,要解决什么问题? fiber是什么? 数据结构 执行单元 浏览器工作: Fiber执行原理 workInProgress tree: currentFiber tree: Effects list: render阶段: 遍历节点过程: 收集effect list: commit阶段: 为什么commit必须是同步的操作的? 为什么要使用fiber,要解决什么问题? 在 react16 引入 Fiber 架构之前,react 会采用递归方法对比两颗虚拟DOM树,找出需

  • 网页资源阻塞浏览器加载的原理示例解析

    目录 正文 测试前环境准备 图片会造成阻塞吗? CSS 加载阻塞 CSS 会阻塞后面 JS 的执行吗? JS 加载阻塞 defer 和 async 动态脚本会造成阻塞吗? DOMContentLoaded 和 onload DOMContentLoaded 遇到脚本 DOMContentLoaded 遇到样式 正文 一个页面允许加载的外部资源有很多,常见的有脚本.样式.字体.图片和视频等,对于这些外部资源究竟是如何影响整个页面的加载和渲染的呢?今天来一探究竟. 如何用 Chrome 定制网络加载

  • Java环境配置原理全面解析

    Java环境配置原理详解 1.Jdk安装目录文件说明: 一般jdk安装目录及路径 \Java\jdk1.7.0_79\lib,里面主要包含以下文件夹. bin:主要存放的是java工具中常用命令如:java,javac等. db:安装java db的路径. include:一些平台特病的头文件. jre:运行java程序所需的jre环境. lib:jdk工具命令的实际存放位置,如:bin中javac命令,实际是lib中tools.jar\sun\tools\javac中的Main.class文件

  • Python程序运行原理图文解析

    本文研究的主要是Python程序运行原理,具体介绍如下. 编译型语言(C语言为例) 动态型语言 一个程序是如何运行起来的?比如下面的代码 #othermodule.py def add(a, b): return a + b #mainrun.py import othermodule a = ['xiaoke', 1, 'python'] a = 'xiaoke string' def func(): a = -5 b = 257 print(a + b) print(a) if __name

  • hashset去除重复值原理实例解析

    Java中的set是一个不包含重复元素的集合,确切地说,是不包含e1.equals(e2)的元素对.Set中允许添加null.Set不能保证集合里元素的顺序. 在往set中添加元素时,如果指定元素不存在,则添加成功.也就是说,如果set中不存在(e==null?e1==null:e.queals(e1))的元素e1,则e1能添加到set中. 下面以set的一个实现类HashSet为例,简单介绍一下set不重复实现的原理: package com.darren.test.overide; publ

  • Java中LinkedList原理代码解析

    本文研究的主要是Java中LinkedList原理的相关内容,具体介绍如下. 一句话概括,Java中的LinkedList其实就是使用双向链表,LinkedList的基本操作就是对双向链表的操作. 上面可以清晰的看出,链表中每个元素对应一个节点,节点里面包含三部分,一个是前一个节点的引用,一个是元素内容,一个是后一个节点的引用. 向链表中添加元素的过程就是在链表尾部追加一个节点 void linkLast(E e) { final Node<E> l = last; final Node<

  • Java继承方法重写实现原理及解析

    这篇文章主要介绍了Java继承方法重写实现原理及解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 在Java继承中,子类可以获得父类所有的结构,也可以增加与父类不同的属性和方法.但是,有一种情况,一个相同的方法,子类的行为并不像父类那样,这时,就需要重写父类的方法,下面是重写的代码实现: 代码体现 package com.my.pac12; /** * @author Summerday * @date 2019/12/11 21:26 */

  • JavaScript事件冒泡机制原理实例解析

    这篇文章主要介绍了JavaScript事件冒泡机制原理实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 DOM事件流(event flow )存在三个阶段:事件捕获阶段.处于目标阶段.事件冒泡阶段,事件冒泡顺序是由内到外进行事件传播,事件冒泡是由IE开发团队提出来的,即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播. 听了简介介绍之后,您可能不理解,所以举个例子: <html> <head>

随机推荐