技术标签: flutter android Flutter vue.js ios
我们知道最终的UI树其实是由一个个独立的Element节点构成。组件最终的Layout、渲染都是通过RenderObject
来完成的,从创建到渲染的大体流程是:根据Widget生成Element,然后创建相应的RenderObject
并关联到Element.renderObject
属性上,最后再通过RenderObject
来完成布局排列和绘制。
Element就是Widget在UI树具体位置的一个实例化对象,大多数Element只有唯一的renderObject
,但还有一些Element会有多个子节点,如继承自RenderObjectElement
的一些类,比如MultiChildRenderObjectElement
。最终所有Element的RenderObject构成一棵树,我们称之为”Render Tree“即”渲染树“。总结一下,我们可以认为Flutter的UI系统包含三棵树:Widget树、Element树、渲染树。他们的依赖关系是:Element树根据Widget树生成,而渲染树又依赖于Element树,如图所示。
现在我们重点看一下Element,Element的生命周期如下:
Widget.createElement
创建一个Element实例,记为element
element.mount(parentElement,newSlot)
,mount方法中首先调用element
所对应Widget的createRenderObject
方法创建与element
相关联的RenderObject对象,然后调用element.attachRenderObject
方法将element.renderObject
添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在Element树结构发生变化时才需要重新attach)。插入到渲染树后的element
就处于“active”状态,处于“active”状态后就可以显示在屏幕上了(可以隐藏)。State.build
返回的Widget结构与之前不同,此时就需要重新构建对应的Element树。为了进行Element复用,在Element重新构建前会先尝试是否可以复用旧树上相同位置的element,element节点在更新前都会调用其对应Widget的canUpdate
方法,如果返回true
,则复用旧Element,旧的Element会使用新Widget配置数据更新,反之则会创建一个新的Element。Widget.canUpdate
主要是判断newWidget
与oldWidget
的runtimeType
和key
是否同时相等,如果同时相等就返回true
,否则就会返回false
。根据这个原理,当我们需要强制更新一个Widget时,可以通过指定不同的Key来避免复用。element
时(如Widget树结构发生了变化,导致element
对应的Widget被移除),这时该祖先Element就会调用deactivateChild
方法来移除它,移除后element.renderObject
也会被从渲染树中移除,然后Framework会调用element.deactivate
方法,这时element
状态变为“inactive”状态。unmount
方法将其彻底移除,这时element的状态为defunct
,它将永远不会再被插入到树中。element
要重新插入到Element树的其它位置,如element
或element
的祖先拥有一个GlobalKey(用于全局复用元素),那么Framework会先将element从现有位置移除,然后再调用其activate
方法,并将其renderObject
重新attach到渲染树。看完Element的生命周期,可能有些读者会有疑问,开发者会直接操作Element树吗?其实对于开发者来说,大多数情况下只需要关注Widget树就行,Flutter框架已经将对Widget树的操作映射到了Element树上,这可以极大的降低复杂度,提高开发效率。但是了解Element对理解整个Flutter UI框架是至关重要的,Flutter正是通过Element这个纽带将Widget和RenderObject关联起来,了解Element层不仅会帮助读者对Flutter UI框架有个清晰的认识,而且也会提高自己的抽象能力和设计能力。另外在有些时候,我们必须得直接使用Element对象来完成一些操作,比如获取主题Theme数据,具体细节将在下文介绍。
我们已经知道,StatelessWidget
和StatefulWidget
的build
方法都会传一个BuildContext
对象:
Widget build(BuildContext context) {}
我们也知道,在很多时候我们都需要使用这个context
做一些事,比如:
Theme.of(context) //获取主题
Navigator.push(context, route) //入栈新路由
Localizations.of(context, type) //获取Local
context.size //获取上下文大小
context.findRenderObject() //查找当前或最近的一个祖先RenderObject
那么BuildContext
到底是什么呢,查看其定义,发现其是一个抽象接口类:
abstract class BuildContext {
...
}
那这个context
对象对应的实现类到底是谁呢?我们顺藤摸瓜,发现build
调用是发生在StatelessWidget
和StatefulWidget
对应的StatelessElement
和StatefulElement
的build
方法中,以StatelessElement
为例:
class StatelessElement extends ComponentElement {
...
@override
Widget build() => widget.build(this);
...
}
发现build
传递的参数是this
,很明显!这个BuildContext
就是StatelessElement
。同样,我们同样发现StatefulWidget
的context
是StatefulElement
。但StatelessElement
和StatefulElement
本身并没有实现BuildContext
接口,继续跟踪代码,发现它们间接继承自Element
类,然后查看Element
类定义,发现Element
类果然实现了BuildContext
接口:
class Element extends DiagnosticableTree implements BuildContext {
...
}
至此真相大白,BuildContext
就是widget对应的Element
,所以我们可以通过context
在StatelessWidget
和StatefulWidget
的build
方法中直接访问Element
对象。我们获取主题数据的代码Theme.of(context)
内部正是调用了Element的dependOnInheritedWidgetOfExactType()
方法。
我们可以看到Element是Flutter UI框架内部连接widget和RenderObject
的纽带,大多数时候开发者只需要关注widget层即可,但是widget层有时候并不能完全屏蔽Element
细节,所以Framework在StatelessWidget
和StatefulWidget
中通过build
方法参数又将Element
对象也传递给了开发者,这样一来,开发者便可以在需要时直接操作Element
对象。那么现在笔者提一个问题:
Element
层是否可以搭建起一个可用的UI框架?如果可以应该是什么样子?对于问题1,答案当然是肯定的,因为我们之前说过widget树只是Element
树的映射,我们完全可以直接通过Element来搭建一个UI框架。下面举一个例子:
我们通过纯粹的Element来模拟一个StatefulWidget
的功能,假设有一个页面,该页面有一个按钮,按钮的文本是一个9位数,点击一次按钮,则对9个数随机排一次序,代码如下:
class HomeView extends ComponentElement{
HomeView(Widget widget) : super(widget);
String text = "123456789";
@override
Widget build() {
Color primary=Theme.of(this).primaryColor; //1
return GestureDetector(
child: Center(
child: TextButton(
child: Text(text, style: TextStyle(color: primary),),
onPressed: () {
var t = text.split("")..shuffle();
text = t.join();
markNeedsBuild(); //点击后将该Element标记为dirty,Element将会rebuild
},
),
),
);
}
}
上面build
方法不接收参数,这一点和在StatelessWidget
和StatefulWidget
中build(BuildContext)
方法不同。代码中需要用到BuildContext
的地方直接用this
代替即可,如代码注释1处Theme.of(this)
参数直接传this
即可,因为当前对象本身就是Element
实例。
当text
发生改变时,我们调用markNeedsBuild()
方法将当前Element标记为dirty即可,标记为dirty的Element会在下一帧中重建。实际上,State.setState()
在内部也是调用的markNeedsBuild()
方法。
上面代码中build方法返回的仍然是一个widget,这是由于Flutter框架中已经有了widget这一层,并且组件库都已经是以widget的形式提供了,如果在Flutter框架中所有组件都像示例的HomeView
一样以Element
形式提供,那么就可以用纯Element
来构建UI了HomeView
的build方法返回值类型就可以是Element
了。
如果我们需要将上面代码在现有Flutter框架中跑起来,那么还是得提供一个“适配器”widget将HomeView
结合到现有框架中,下面CustomHome
就相当于“适配器”:
class HomeCustomPage extends StatelessWidget {
const HomeCustomPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(appBar: getAppBar("自定义UI"),body: CustomHome(),);
}
}
class CustomHome extends Widget{
@override
Element createElement() {
return HomeView(this);
}
}
点击按钮则按钮文本会随机排序。
对于问题2,答案当然也是肯定的,Flutter 引擎提供的 API是原始且独立的,这个与操作系统提供的API类似,上层UI框架设计成什么样完全取决于设计者,完全可以将UI框架设计成 Android 风格或 iOS 风格,但这些事Google不会再去做,我们也没必要再去搞这一套,这是因为响应式的思想本身是很棒的,之所以提出这个问题,是因为笔者认为做与不做是一回事,但知道能不能做是另一回事,这能反映出我们对知识的理解程度。
每个Element
都对应一个RenderObject
,我们可以通过Element.renderObject
来获取。并且我们也说过RenderObject
的主要职责是Layout和绘制,所有的RenderObject
会组成一棵渲染树Render Tree。本节我们将重点介绍一下RenderObject
的作用。
RenderObject
就是渲染树中的一个对象,它主要的作用是实现事件响应以及渲染管线中除过 build 的部分(build 部分由 element 实现),即包括:布局、绘制、层合成以及上屏,这些我们将在后面章节介绍。
RenderObject
拥有一个parent
和一个parentData
插槽(slot),所谓插槽,就是指预留的一个接口或位置,这个接口和位置是由其它对象来接入或占据的,这个接口或位置在软件中通常用预留变量来表示,而parentData
正是一个预留变量,它正是由parent
来赋值的,parent
通常会通过子RenderObject
的parentData
存储一些和子元素相关的数据,如在Stack布局中,RenderStack
就会将子元素的偏移数据存储在子元素的parentData
中(具体可以查看Positioned
实现)。
RenderObject
类本身实现了一套基础的layout和绘制协议,但是并没有定义子节点模型(如一个节点可以有几个子节点,没有子节点?一个?两个?或者更多?)。 它也没有定义坐标系统(如子节点定位是在笛卡尔坐标中还是极坐标?)和具体的布局协议(是通过宽高还是通过constraint和size?,或者是否由父节点在子节点布局之前或之后设置子节点的大小和位置等)。
为此,Flutter框架提供了一个RenderBox
和一个 RenderSliver
类,它们都是继承自RenderObject
,布局坐标系统采用笛卡尔坐标系,这和Android和iOS原生坐标系是一致的,都是屏幕的top、left是原点,然后分宽高两个轴,大多数情况下。而 Flutter 基于这两个类分别实现了基于 RenderBox 的盒模型布局和基于 Sliver 的按需加载模型。
<appSettings> <add key="ffmpeg" value="E:\ffmpeg\ffmpeg-20141012-git-20df026-win32-static\bin\ffmpeg.exe" /> <add key="CatchFlvImgSize" value="240x180" /></appSettings&...
学习Python就等于学习人工智能吗?当然不!千万不要混淆它们的概念!Python工程师并不等同于人工智能工程师。(关注公众号"猎维人工智能”)Python工程师与人工智能工程师最根本的区别是什么?Python和JAVA、C语言一样,是一种编程语言,所以从事Python开发的人员也只能算是程序员。而人工智能工程师是从事算法研究的,所以被称作算法工程师。一个是程序员,一个是算法工程师。这两...
查看所有POD:kubectl get pod查看某个POD: kubectl get pod name以JSON格式输出POD信息: kubectl get pod name --output json以yaml格式输出POD信息: kubectl get pod name --output yaml删除某个POD:kubectl delete pod name删除所有POD(慎用):k...
Qt是一个跨平台框架,通常用作图形工具包,它不仅创建CLI应用程序中非常有用。而且它也可以在三种主要的台式机操作系统以及移动操作系统(如Symbian,Nokia Belle,Meego Harmattan,MeeGo或BB10)以及嵌入式设备,Android(Necessitas)和iOS的端口上运行。现在我们为你提供了免费的试用版。下载Qt6最新试用版Qt组件推荐:QtitanRibbon|下载试用:遵循Microsoft Ribbon UI Paradigm for Qt技术的Ribb..
介绍 (Introduction)If you are actively developing an application, using Docker can simplify your workflow and the process of deploying your application to production. Working with containers in develo...
学习链接:100道MySQL常见面试题总结写给 Java 程序员的 24 个MySQL面试题,拿走不谢!写给 Java 程序员的 24 个MySQL面试题,拿走不谢! 转MySQL经典面试题玩转Mysql系列 - 第24篇:如何正确的使用索引?...
一、爬虫部分爬虫说明:1、本爬虫是以面向对象的方式进行代码架构的2、本爬虫爬取的数据存入到MongoDB数据库中3、爬虫代码中有详细注释4、爬虫爬取的美食是以无锡为例代码展示import jsonimport reimport timefrom pymongo import MongoClientimport requestsfrom lxml import htmlclass MeituanSpider(): def __init__(self): #
java的基本特征1.简单Java最初是为对家用电器进行集成控制而设计的一种语言,因此它必须简单明了。Java语言的简单性主要体现在以下三个方面:Java的风格类似于C++,因而C++程序员是非常熟悉的。从某种意义上讲,Java语言是C及C++语言的一个变种,因此,C++程序员可以很快就掌握Java编程技术。Java摒弃了C++中容易引发程序错误的地方,如指针和内存管理。Ja...
安装Roboguide软件的详细步骤
node-webkit中使用sqlite3sqlite3的官方文档提到:nodejs和node-webkit的ABI不同,所以通过npm install sqlite3下载的sqlite3是无法使用的,需要重新编译。windows编译:以LTS版本(0.14.7)为例一、所需编译环境安装Python 2.7.14(不支持3.x版本)并设置好环境变量,下载地址:https://www.python.org/downloads/ 安装最新的nodejs+npm 下载地址:https...
题目链接:http://acm.hust.edu.cn/vjudge/problem/51191题意:给一个字符串,相同部分可以折叠,折叠可以嵌套。求最短长度的一种折叠方法。括号和数字的长度也要考虑进去。思路:对于一个字符串,有三种策略:1、不折叠。2、本身可以折叠。3、分为两个区间子问题。#include #include #include #inclu