这篇文章基于 Flutter stable v1.7 总结下 Flutter 当前的 UI 系统以及相关的概念, 在最后会通过自己组合一个 Gradient Button 按钮的方式来熟悉 Flutter 的一些 UI 实现。

Flutter 框架整体分层:

下面会主要关注 Widgets 和 Rendering, 也会涉及到一点 Painting 部分的内容。

Flutter 三层树

React 中,Component 对象并不是真正负责渲染,需要框架生成 element 后,进行初始渲染或者 diff 后 patch 变更。Flutter 也借鉴了 React 的设计,Widget 并不是真正的渲染对象,真正的渲染是经历了从 Widget => Element=> RenderObject 的过程,与 React 拼接成 dom 交给浏览器渲染不同,Flutter 中 RenderObject 负责来在绘制引擎的上层进行绘制。

先来看下类分布及其职责:

-- Widget 存储配置信息,另外其由于是 immutable 的,所以会不断重新创建刷新。-- Element 是分离 Widget 和真正渲染对象的中间层,很多控制渲染的行为在这层去处理。-- RenderObject 来真正的执行 Diff, Hit Test, 布局以及绘制。

三层对象构成的树之间的关联则如下图表示:

如图可知,Widget 会创建 Element,然后 Element 创建相应的 RenderObject,Element 就是 Widget 在 UI 树具体位置的一个对象,一个 Widget 可能有多个 Element,大多的 Element 只有唯一的 renderObject,但也有一些 Element 会有多个子节点,比如 MultiChildRenderObjectElement。最终所有 RenderObject 构成 render tree 。

Stateful & Stateless Widget

关于 StatefulWidget 以及 StatelessWidget 的使用,已经有很多的文章进行了描述,这些 Widget 可能不是传统原生开发中的 UI 控件,也可能是布局,语义化或者主题等等,通过这些组件组合堆叠,来完成应用界面的绘制。

在一开始接触 Flutter 时会有点抵触这个 State 职责定位,因为涉及到构建树的 build 方法也需要在 State 中重载。不过整体看下来,这么设计也是基于 Flutter 的一部分机制导致的,首先 Widget 本身也分为 UI 和功能型,另外 Widget 树会随着 State 的变化而变化,每次 build 都会重新生成树,Widget 会频繁的销毁重建而 State 对象则不会,所以这么看来由 State 对象来返回树会更合适一些。

State 的生命周期

  • initState 方法会在 State 初始化时调用,由于 State 对象会被 Framework 长期持有,所以该方法在其生命周期中只调用一次,我们经常会在这做一些一次性的操作,比如状态初始化,订阅事件等。

  • didChangeDependencies 这个方法会在初始化后调用一次,直到 State 对象的依赖发生变化时才会被调用,比如上层的树中包含 InheritedWidget,如果其发生了变化,那么此时 InhertiedWidget 的子 widget 的 didChangeDependencies() 都会被调用。典型的场景是一些全局的配置比如 Locale, Theme, 或者 Redux 的 StoreProvider 组件改变会导致子树收到该消息并 re-build 组件树。另外如果有些行为不希望在每次 build 都触发,也可以考虑到将其放到 didChangeDenpendencies 中来。

  • didUpdateWidget 当 widget 的配置变化时,会调用该方法并触发 build

  • build 返回改组件构建的树结构

  • deactivate 当前组件暂时被从视图树中移除时会调用该方法,比如页面切换或者应用挂起都会触发这个方法。

  • dispose 永久移除时作为析构函数,可以在这里做一些资源的释放操作。

上面有提到 InheritedWidget,这个类的存在主要来解决需要逐层传递 State 的问题,当我们有 State 需要共享时,就可以将其放在一个继承 InheritedWidget 的类中,然后使用的组件直接取用就可以。常见与其相关的场景比如 Theme.of(context), Locale.of(context), ButtonTheme.of(context) 等等。

Element

Element 的生命周期

  1. Widget.createElement 创建一个 Element 实例。

  2. element.mount() 会让其 widget createRenderObject 并将对象 attach 到渲染树中插槽指定的位置,插入后该 element 标记为 'active' 状态。

  3. 当 widget 的配置数据改变时,为了对 element 进行复用,Framework 在决定重新创建 Element 前会先尝试复用相同位置旧的 element, 调用对应的 widget 的 canUpdate() 方法来确定是否更新,canUpdate()方法主要判断新旧 widget 的 runtimeType 以及 key, 所以我们可以通过指定不同的 Key 来强制刷新。

  4. 当有祖先元素决定要移除 element 时,会调用 deactivateChild 方法来移除孩子,移除后 element.renderObject 也会被从渲染树中移除,然后 Framework 会调用 element.deactivate 方法,这时 element 标记为 'inactive' 状态。

  5. 'inactive' 态的 element 将不会再显示到屏幕。为了避免在一次动画执行过程中反复创建、移除某个特定 element, 'inactive' 态的 element 在当前动画最后一帧结束前都会保留,如果动画结束后不能重新 'active', 则会调用 unmount 方法将其彻底移除,这是 element 状态标为 defunct 。

  6. 如果 element 要重新插入到 Element 树其它位置,如 element 或 element 的祖先拥有一个 GlobalKey, 那么 Framework 会先将 element 从现有位置移除,然后再调用 activate 方法,并将其 renderObject 重新 attach 到渲染树。

BuildContext

我们在 build widgets 时都会重载 build 方法,build 方法有一个参数就是 BuildContext 上下文对象,我们可以拿到 context 去其祖先寻找指定类型的对象,比如 Theme, Navigator, Localizations 等等。

文档表示的很清楚,BuildContext 对象实际上就是 Element 对象, 它设计成抽象接口类主要为了屏蔽使用者对 Element 进行操作。而我们常用的 of 操作则是调用了 Element 的 inheritFromWidgetOfExactType() 方法。

以 Theme.of(context) 为例,我们可以看到它的实现:

这里会调用 context 也就是 element 的 inheritFromWidgetOfExactType 方法拿到指定类型的对象,返回一个经过本地化的主题数据。

inheritFromWidgetOfExactType 方法也很简单,直接从缓存的哈希表里找到拥有指定对象类型的祖先节点,然后返回给调用方,缓存中没有则添加到缓存中。另外值得注意的是,这里的哈希表就是之前在 Widget state 里提到的 dependencies 。

BuildOwner

上文看到 BuildContext 中有一个 getter 方法来获取 buildOwner, 那么 BuildOwner 具体会做些什么呢?

它作为 widgets 的管理者,会追踪哪些 widget 需要重建,并且处理一些组件树上的其他任务比如管理 inactive 的 element 列表,或者触发 reassemble 命令当 hot reload 的时候。最主要的 build owner 是由 WidgetsBinding 持有的,它会被操作系统驱动去执行 build/layout/paint 的 pipeline。

另外 build owners 也被用来管理 off-screen 的组件树,谷歌官方介绍组件树的视频有提到,每当 widget 改变需要重新渲染时,framework 会在绘制的 idle time 去计算要新渲染的树,完成后直接对当前的进行替换并渲染。利用 idle time 的方式也很类似 React 的 Fiber 设计 (利用浏览器 requestIdleCallback api)。

布局及绘制

Flutter 界面渲染过程分为三个阶段:布局,绘制和合成,布局和绘制会在 Flutter 框架中完成,而合成则交给引擎负责:

前文有提到每个 Element 都会对应一个 RenderObject,它的职责主要则是布局和绘制,所有的 RenderObject 会组成一棵渲染树 RenderTree 。

RenderObject 拥有一个 parent 和一个 parentData 插槽,parentData 这个预留变量正是由 parent 来赋值,parent 通常会通过子 RenderObject 来存储一些和子元素相关的数据比如偏移量。当然,其不仅仅可以存储偏移信息,通常所有和子节点特定的数据都可以存储到子节点的 parentData 中,如 ContainerBox 中该属性就保存了指向兄弟节点的 previousSibling 和 nextSibling,Element 的 visitChildren() 方法也是通过它们来实现对子节点的同层级(广度)遍历。

RenderObject 类本身实现了一套基础的 layout 和绘制协议,但是并没有定义子节点模型,坐标系统以及具体的布局协议。为此,Flutter 提供了一个 RenderBox 类,继承自 RenderObject,坐标系采用笛卡尔坐标系。

布局过程

渲染树种每个节点都会接受父节点的 Contraints 参数,决定自己大小,然后父节点就可以按照自己的逻辑决定各个子节点的位置,完成布局过程。

具体来看,RenderBox 中有一个 size 属性用来保存宽高,RenderBox 的 layout 是通过在组件树上从上往下传递 BoxConstraints 对象实现的,它可以限制子节点的最大和最小宽高,布局阶段,父节点会调用子节点的 layout() 方法,大致实现如下:

布局前先要确定 relayoutBoundary,该参数标识当前节点是否是布局边界。即当边界内的节点发生重新布局时,不会影响边界外的节点。

在 Element 层,如果其被标记为 dirty 时(通过 markNeedsBuild())则会重新 build,这时 RenderObject 便会重新布局。在 RenderObject 中则有一个 markNeedsLayout() 方法,它会将 RenderObject 的布局状态标记为 dirty,这样在下一帧便会重新 layout,我们来看下该方法:

确定 relayoutBoundary 是不是自己,不是则继续向上寻找,是则告知 buildOwner 当前节点需要布局,并调用了更新方法。

另外在 layout() 方法中我们看到其是通过 performLayout() 来去真正的执行布局,则总结起来调用顺序为: layout() > performResize()/perforLayout() > child.layout() > ... 如此递归完成整个 UI 树的布局。如果需要子类化 RenderBox 类来定制布局,则应该通过重写 performResize 和 performLayout 来实现,而不是 layout 。

另外有一个问题,除非显式的指定 size,很多控件在 build 时是不清楚具体尺寸的,但很多时候我们需要提前清楚 size 来做一些操作或者布局,闲鱼团队的 深入了解Flutter界面开发 中有提到可以在 layout 阶段后发送一个 notification 通知上层,另外也可以并推荐的方式是通过胶水层 WidgetsBinding 注册一个 PostFrameCallback (WidgetsBinding.instance.addPostFrameCallback),然后回调里通过

_listViewKey.currentContext.findRenderObject().paintBounds.size.width;

的方式来拿到尺寸。

绘制过程

RenderObject 可以通过 paint() 方法来完成具体绘制逻辑,流程和布局类似。这里以 RenderFlex 的 paint 方法为例说明:

defaultPaint:

由于 Flex 属于一个布局类,自身没有需要绘制的部分,则直接遍历子节点并调用 paintChild 方法触发子节点绘制。

渲染流程中也有个与 relayoutBoundary 对应的属性 repaintBoundary,用于确定重绘边界,提高绘制效率,避免绘制的干扰以及不必要的重绘。

RenderObject 中有一个 isRepaintBoundary 属性,决定重绘时是否独立于其父元素,若为 true 则单独建立图层绘制。可以看下 paintChild() 方法:

如果子节点是 repaintBoundary 则会调用 _compositeChild 方法并将偏移量传递过去,不是则直接从上下文进行绘制。

另外看下触发重绘的 markNeedsPaint() 方法:

与 layout 类似,会判断边界并交给 pipeline owner 去做相关工作。

在 iOS 中,不同职责的 layer 组合在一起组成了 view,Flutter 中刚才可以看到如果有一个 repaintBoundary 区域形成时,框架会创建一个 Layer,不同 Lyaer 也是可以独立工作的,比如 OffsetLayer 在 RenderObject 中就是用来做定位绘制的。

其次在 RenderObject 中有一个属性为 needsCompositing,它会影响生成多少层的 Layer, 而这些 Layer 又会组成一棵 Layer Tree ... 也就是实际去给引擎绘制的树。

实现一个 Button

最后,我们会以框架实现 Button 的方式来实现一个带渐变背景的按钮:

语义化在最顶层,使用 Semantics 来包装整个 Widget;管理焦点(1.7 新增)使用 Focus;布局和焦点用到两个 'Box',分别为 ConstrainedBox 和 DecoratedBox;样式及点击水波效果通过 Material 和 Inkwell;Default 的样式则通过全局的 ThemeButtonTheme 和 IconTheme 来控制;

综上,build 方法大概如下:

-- EOF --以上为本篇文章的全部内容,欢迎提出建议和指正,

参考资源:

  • Github/flutter

  • flutter

  • RenderObject和RenderBox

  • 深入了解Flutter界面开发

  • Flutter的原理及美团的实践

  • 深入绘制原理

  • 基于JS的高性能Flutter动态化框架MXFlutter

  • flutter-playlist-youtube

element中有多个合计_深入理解 Flutter 中的 Widget, Element, RenderObject相关推荐

  1. python iterable对象_如何理解Python中的iterable对象

    转载请注明出处:https://www.jianshu.com/u/5e6f798c903a [^*] 表示注脚,在文末可以查看对应连接,但简书不支持该语法. 首先,容器和 iterable 间没有必 ...

  2. python的上下文管理用哪个关键字_正确理解python中的关键字“with”与上下文管理器...

    正确理解python中的关键字"with"与上下文管理器 来源:中文源码网    浏览: 次    日期:2018年9月2日 [下载文档:  正确理解python中的关键字&quo ...

  3. java中io是什么_深入理解Java中的IO

    深入理解Java中的IO 转载自:http://blog.csdn.net/qq_25184739/article/details/51205186 本文的目录视图如下: Java IO概要 a.Ja ...

  4. python参数传递方法_深入理解python中函数传递参数是值传递还是引用传递

    python 的 深入理解python中函数传递参数是值传递还是引用传递 目前网络上大部分博客的结论都是这样的: Python不允许程序员选择采用传值还是传 引用.Python参数传递采用的肯定是&q ...

  5. python中继承是什么意思_如何理解Python中的继承?python入门

    如何理解Python中的继承?如今,python编程语言深受企业和个人的喜爱.python开发工程师是近年来互联网行业非常热门的职业岗位之一.学习python的人除了零基础的,还有一部分是在职运维.在 ...

  6. java中demo接人_return的用法_如何理解java中return的用法?

    C语言中return用法?(请熟练者进) return是返回值,这个返回值是和函数的类型有关的,函数的类型是什么,他的返回值就是什么 比方主函数intmain() {}这里就必须有一个return,只 ...

  7. java虚引用作用_深入理解Java中的引用(二)——强软弱虚引用

    深入理解Java中的引用(二)--强软弱虚引用 在上一篇文章中介绍了Java的Reference类,本篇文章介绍他的四个子类:强引用.软引用.弱引用.虚引用. 强引用(StrongReference) ...

  8. pythonself用法_全面理解python中self的用法

    self代表类的实例,而非类. classTest:defprt(self):print(self)print(self.__class__) t=Test() t.prt() 执行结果如下 从上面的 ...

  9. python变量的理解_如何理解Python中的变量

    在本篇文章里小编给大家分享的是关于Python中变量是什么意思的相关基础知识点,需要的朋友们可以学习下. 变量 在Python中,存储一个数据,需要定义一个变量 number1 = 1 #numbe1 ...

最新文章

  1. 微服务常见安全认证方案Session token cookie跨域
  2. 反射的本质——元数据
  3. 【转】一步步构建大型网站架构
  4. 在 Ubuntu 16.04 LTS 上安装 Python 3.6.0
  5. 2008服务器系统功能,Windows Server 2008 DNS服务器新增功能
  6. 公式中*和· 号的含义区分(GRU公式)
  7. 从“No space left on device”到删除海量文件
  8. XQuery FLWOR 表达式
  9. 什么是协方差(covariance)?(延伸到 协方差矩阵、多元高斯分布、PCA)
  10. 实现迭代服务器端和客户端
  11. Switchhosts软件安装包
  12. 2008年管理软件行业的七大趋势预测
  13. SPSS数据插补方法
  14. 深度Q学习——从入门到实践
  15. 模仿淘宝首页html+js+css(附带源码)
  16. 金园云化工园区智慧应急解决方案
  17. 有道云笔记 markdown html,有道云笔记Markdown之甘特图
  18. 深入解析J.U.C并发包(十五)—— Thread - Specific Storage(ThreadLocal)模式
  19. elasticsearch搜索引擎搭建
  20. java 给文件加密文件_java对文件做简单加密的方法

热门文章

  1. java 命令行 读取文件_java读取txt文件
  2. 无线路由器发起ARP攻击,致使网络中断,这是为什么?
  3. python 文件操作 os.mkdir()函数
  4. QT安卓web使用mysql_Qt使用MySQL笔记一
  5. Java进阶:ReentrantLock和Condition基本使用
  6. android imageview 锯齿,android 自定义圆角ImageView以及锯齿的处理
  7. liunx系统中的盘符能修改嘛_装系统教程!如何从U盘启动(中)!小白也能变装机大神!...
  8. 使用shell脚本完成自动化部署jar包
  9. shiro登录认证过程讲解(转)
  10. 安卓怎么显示res文件夹中的html_使用Android WebView加载现有的.html文件