diff --git a/docs/chapter1/dart.md b/docs/chapter1/dart.md index b42cd4eb..394f11cc 100644 --- a/docs/chapter1/dart.md +++ b/docs/chapter1/dart.md @@ -1,6 +1,6 @@ # Dart语言简介 -在之前我们已经介绍过Dart语言的相关特性,读者可以翻看一下,如果你熟悉Dart语法,可以跳过本节,如果你还不了解Dart,不用担心,按照笔者经验,如果你有过其他编程语言经验,尤其是Java和JavaScript的话,所以,如果你是前端或Android开发者,那么将会非常容易上手Dart。当然,如果你是iOS开发者,也不用担心,dart中也有一些与swift比较相似的特性,如命名参数等,笔者当时学习Dart时,只是花了一个小时,看完Dart官网的Language Tour,就开始动手写Fluttter了。 +在之前我们已经介绍过Dart语言的相关特性,读者可以翻看一下,如果你熟悉Dart语法,可以跳过本节,如果你还不了解Dart,不用担心,按照笔者经验,如果你有过其他编程语言经验,尤其是Java和JavaScript的话,所以,如果你是前端或Android开发者,那么将会非常容易上手Dart。当然,如果你是iOS开发者,也不用担心,dart中也有一些与swift比较相似的特性,如命名参数等,笔者当时学习Dart时,只是花了一个小时,看完Dart官网的Language Tour,就开始动手写Flutter了。 在笔者看来,Dart的设计目标应该是既对标Java,也对标JavaScript,Dart在静态语法方面和Java非常相似,如类型定义、函数声明、泛型等,而在动态特性方面又和JavaScript很像,如函数式特性、异步支持等。除了融合Java和JavaScript语言之所长之外,Dart也具有一些其它具有表现力的语法,如可选命名参数、`..`(级联运算符)和`?.`(条件成员访问运算符)以及`??`(判空赋值运算符)。其实,对编程语言了解比较多的读者会发现,在Dart中其实看到的不仅有Java和JavaScript的影子,它还具有其它编程语言中的身影,如命名参数在Objective-C和Swift中早就很普遍,而`??`操作符在Php 7.0语法中就已经存在了,因此我们可以看到Google对Dart语言给予厚望,是想把Dart打造成一门集百家之所长的编程语言。 diff --git a/docs/chapter1/mobile_development_intro.md b/docs/chapter1/mobile_development_intro.md index 2601d731..52ccaaf8 100644 --- a/docs/chapter1/mobile_development_intro.md +++ b/docs/chapter1/mobile_development_intro.md @@ -131,12 +131,12 @@ React中提出一个重要思想:状态改变则UI随之自动改变,而Reac ### React Native -上文已经提到React Native 是React 在原生移动应用平台的衍生产物,那两者主要的区别是什么呢?其实,主要的区别在于虚拟DOM映射的对象是什么?React中虚拟DOM最终会映射为浏览器DOM树,而RN中虚拟DOM会通过JavaScript Core 映射为原生控件树。 +上文已经提到React Native 是React 在原生移动应用平台的衍生产物,那两者主要的区别是什么呢?其实,主要的区别在于虚拟DOM映射的对象是什么?React中虚拟DOM最终会映射为浏览器DOM树,而RN中虚拟DOM会通过 JavaScriptCore 映射为原生控件树。 -JavaScript Core 是一个JavaScript解释器,它在React Native中主要有两个作用: +JavaScriptCore 是一个JavaScript解释器,它在React Native中主要有两个作用: 1. 为JavaScript提供运行环境。 -2. 是JavaScript与原生应用之间通信的桥梁,作用和JsBridge一样,事实上,在iOS中,很多JsBridge的实现都是基于JavaScript Core 。 +2. 是JavaScript与原生应用之间通信的桥梁,作用和JsBridge一样,事实上,在iOS中,很多JsBridge的实现都是基于 JavaScriptCore 。 而RN中将虚拟DOM映射为原生控件的过程中分两步: diff --git a/docs/chapter13/custom_paint.md b/docs/chapter13/custom_paint.md index fad2171d..f18e9edd 100644 --- a/docs/chapter13/custom_paint.md +++ b/docs/chapter13/custom_paint.md @@ -160,7 +160,7 @@ class MyPainter extends CustomPainter { 绘制是比较昂贵的操作,所以我们在实现自绘控件时应该考虑到性能开销,下面是两条关于性能优化的建议: -- 尽可能的利用好`shouldRepaint`返回值;在UI树重新build时,控件在绘制前都会先调用该方法以确定是否有必要重绘;加入我们绘制的UI不依赖外部状态,那么就应该始终返回false,因为外部状态改变导致重新build时不会影响我们的UI外观;如果绘制依赖外部状态,那么我们就应该在shouldRepaint中判断依赖的状态是否改变,如果已改变则应返回`true`来重绘,反之则应返回`false`不需要重绘。 +- 尽可能的利用好`shouldRepaint`返回值;在UI树重新build时,控件在绘制前都会先调用该方法以确定是否有必要重绘;假如我们绘制的UI不依赖外部状态,那么就应该始终返回false,因为外部状态改变导致重新build时不会影响我们的UI外观;如果绘制依赖外部状态,那么我们就应该在shouldRepaint中判断依赖的状态是否改变,如果已改变则应返回`true`来重绘,反之则应返回`false`不需要重绘。 - 绘制尽可能多的分层;在上面五子棋的示例中,我们将棋盘和棋子的绘制放在了一起,这样会有一个问题:由于棋盘始终是不变的,用户每次落子时变的只是棋子,但是如果按照上面的代码来实现,每次绘制棋子时都要重新绘制一次棋盘,这是没必要的。优化的方法就是将棋盘单独抽为一个Widget,并设置其`shouldRepaint`回调值为false,然后将棋盘Widget作为背景。然后将棋子的绘制放到另一个Widget中,这样落子时只需要绘制棋子。 diff --git a/docs/chapter13/intro.md b/docs/chapter13/intro.md index d0038163..da5badd8 100644 --- a/docs/chapter13/intro.md +++ b/docs/chapter13/intro.md @@ -26,6 +26,6 @@ PaintingContext代表Widget的绘制上下文,通过`PaintingContext.canvas` ### 总结 -组合是自定义组件最简单的方法,在任何需要自定义的场景下,都应该优先考虑是否能够通过组合来实现。而自绘和通过实现RenderObject的方法本质上是一样的,都需要开发者调用Canvas API手动去绘制UI,缺点时必须了解Canvas API,并且得自己去实现绘制逻辑,而优点是强大灵活,理论上可以实现任何外观的UI。 +组合是自定义组件最简单的方法,在任何需要自定义的场景下,都应该优先考虑是否能够通过组合来实现。而自绘和通过实现RenderObject的方法本质上是一样的,都需要开发者调用Canvas API手动去绘制UI,缺点是必须了解Canvas API,并且得自己去实现绘制逻辑,而优点是强大灵活,理论上可以实现任何外观的UI。 在本章接下来的小节中,我们将通过一些实例来详细介绍自定义UI的过程,由于后两种方法本质是相同的,后续我们只介绍CustomPaint和Canvas的方式,读者如果对自定义RenderObject的方法好奇,可以查看RenderParagraph或RenderImage源码。 diff --git a/docs/chapter14/element_buildcontext.md b/docs/chapter14/element_buildcontext.md index 5bbde61f..8a7c5d40 100644 --- a/docs/chapter14/element_buildcontext.md +++ b/docs/chapter14/element_buildcontext.md @@ -2,17 +2,17 @@ ### Element -在“Widget简介”一节,我们介绍了Widget和Element的关系,我们知道最终的UI树其实一个个独立的Element节点构成。我们也知道了组件最终的Layout、渲染都是通过RenderObject来完成的,从创建到渲染的大体流程是:根据Widget生成Element,然后创建将相应的RenderObject关联到Element.renderObject属性,最后再通过RenderObject来完成布局排列和绘制。 +在“Widget简介”一节,我们介绍了Widget和Element的关系,我们知道最终的UI树其实是由一个个独立的Element节点构成。我们也知道了组件最终的Layout、渲染都是通过RenderObject来完成的,从创建到渲染的大体流程是:根据Widget生成Element,然后创建相应的RenderObject并关联到Element.renderObject属性上,最后再通过RenderObject来完成布局排列和绘制。 Element就是Widget在UI树具体位置的一个实例化对象,大多数Element只有唯一的renderObject,但还有一些Element会有多个子节点,如继承自RenderObjectElement的一些类,比如MultiChildRenderObjectElement。最终所有Element的RenderObject构成一棵树,我们称之为渲染树,即render tree。 Element的生命周期如下: 1. Framework 调用`Widget.createElement` 创建一个Element实例,记为`element` -2. Framework 调用 `element.mount(parentElement,newSlot)` ,mount方法中首先调用`elment`所对应Widget的`createRenderObject`方法创建与`element`相关联RenderObject对象,然后调用`element.attachRenderObject`方法将`element.renderObject`添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在Element树结构发生变化时才需要重新attach)。插入到渲染树后的`element`就处于“active”状态,处于“active”状态后就可以显示在屏幕上了(可以隐藏)。 +2. Framework 调用 `element.mount(parentElement,newSlot)` ,mount方法中首先调用`elment`所对应Widget的`createRenderObject`方法创建与`element`相关联的RenderObject对象,然后调用`element.attachRenderObject`方法将`element.renderObject`添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在Element树结构发生变化时才需要重新attach)。插入到渲染树后的`element`就处于“active”状态,处于“active”状态后就可以显示在屏幕上了(可以隐藏)。 3. 当`element`父Widget的配置数据改变时,为了进行Element复用,Framework在决定重新创建Element前会先尝试复用相同位置旧的element:调用Element对应Widget的`canUpdate()`方法,如果返回`true`,则复用旧Element,旧的Element会使用新的Widget配置数据更新,反之则会创建一个新的Element,不会复用。`Widget.canUpdate()`主要是判断`newWidget`与`oldWidget`的`runtimeType`和`key`是否同时相等,如果同时相等就返回`true`,否则就会返回`false`。根据这个原理,当我们需要强制更新一个Widget时,可以通过指定不同的Key来禁止复用。 4. 当有父Widget的配置数据改变时,同时其`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来避免复用。 -5. 当有祖先Element决定要移除`element ` 时(如Widget树结构发生了变化,导致`element`对应的Widget被移除),这是该祖先Element就会调用`deactivateChild` 方法来移除它,移除后`element.renderObject`也会被从渲染树中移除,然后Framework会调用`element.deactivate ` 方法,这时`element`状态变为“inactive”状态。 +5. 当有祖先Element决定要移除`element ` 时(如Widget树结构发生了变化,导致`element`对应的Widget被移除),这时该祖先Element就会调用`deactivateChild` 方法来移除它,移除后`element.renderObject`也会被从渲染树中移除,然后Framework会调用`element.deactivate ` 方法,这时`element`状态变为“inactive”状态。 6. “inactive”态的element将不会再显示到屏幕。为了避免在一次动画执行过程中反复创建、移除某个特定element,“inactive”态的element在当前动画最后一帧结束前都会保留,如果在动画执行结束后它还未能重新变成”active“状态,Framework就会调用其`unmount`方法将其彻底移除,这时element的状态为`defunct`,它将永远不会再被插入到树中。 7. 如果`element`要重新插入到Element树的其它位置,如`element`或`element`的祖先拥有一个GlobalKey(用于全局复用元素),那么Framework会先将element从现有位置移除,然后再调用其`activate`方法,并将其`renderObject`重新attach到渲染树。 @@ -44,7 +44,7 @@ abstract class BuildContext { } ``` -那StatelessWidget和StatefulWidget的build方法传入的context对象是哪个实现了BuildContext的类。我们顺藤摸瓜,发现调用时发生在StatelessWidget和StatefulWidget对应的StatelessElement和StatefulElement的build犯法中,以StatelessElement为例: +那StatelessWidget和StatefulWidget的build方法传入的context对象是哪个实现了BuildContext的类。我们顺藤摸瓜,发现调用时发生在StatelessWidget和StatefulWidget对应的StatelessElement和StatefulElement的build方法中,以StatelessElement为例: ```dart @@ -72,7 +72,7 @@ class Element extends DiagnosticableTree implements BuildContext { 我们可以看到Element是Flutter UI框架内部连接Widget和RenderObject的纽带,大多数时候开发者只需要关注Widget层即可,但是Widget层有时候并不能完全屏蔽Element细节,所以Framework在StatelessWidget和StatefulWidget中通过build方法参数将Element对象也传递给了开发者,这样便可以在需要时直接操作Element对象。那么现在笔者提两个问题,请读者先自己思考一下: -1. 如果没有Widget层,单靠Element层是否可以搭建起一个完成的UI框架?如果可以应该是什么样子? +1. 如果没有Widget层,单靠Element层是否可以搭建起一个可用的UI框架?如果可以应该是什么样子? 2. Flutter UI框架能不做成响应式吗? 对于问题1,答案当然是肯定的,因为我们之前说过Widget树只是Element树的映射,我们完全可以直接通过Element来搭建一个UI框架。下面举一个例子: @@ -106,7 +106,7 @@ class HomeView extends ComponentElement{ - 上面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了。 +- 上面代码中build方法返回的仍然是一个Widget,这是由于Flutter框架中已经有了Widget这一层,并且组件库都已经是以Widget的形式提供了,如果在Flutter框架中所有组件都像示例的HomeView一样以Element形式提供,那么就可以用纯Element来构建UI了,HomeView的build方法返回值类型就可以是Element了。 如果我们需要将上面代码在现有Flutter框架中跑起来,那么还是得提供一个”适配器“Widget将HomeView结合到现有框架中,下面CustomHome就相当于”适配器“: @@ -125,7 +125,8 @@ class CustomHome extends Widget { 点击按钮则按钮文本会随机排序。 -对于问题2,答案当然也是肯定的,Flutter engine提供的dart API是原始且独立的,这个操作系统提供的类似,上层UI框架设计成什么样完全取决于设计者,完全可以将UI框架设计成Android风格或iOS风格,但这些事Google不会再去做,当然没有十足的理由我们也没必要再去搞一套,这是因为响应式的思想本身是很棒的,之所以提出这个问题,是因为笔者认为但做与不做是一回事,但知道能与不能是另一回事,这能反映出我们对知识的掌握程度。 +对于问题2,答案当然也是肯定的,Flutter engine提供的dart API是原始且独立的,这个与操作系统提供的API类似,上层UI框架设计成什么样完全取决于设计者,完全可以将UI框架设计成Android风格或iOS风格,但这些事Google不会再去做,我们也没必要再去搞这一套,这是因为响应式的思想本身是很棒的,之所以提出这个问题,是因为笔者认为做与不做是一回事,但知道能不能做是另一回事,这能反映出我们对知识的掌握程度。 + ### 总结 diff --git a/docs/chapter14/flutter_ui_system.md b/docs/chapter14/flutter_ui_system.md index 5f17137b..a3fced6d 100644 --- a/docs/chapter14/flutter_ui_system.md +++ b/docs/chapter14/flutter_ui_system.md @@ -20,11 +20,11 @@ CPU和GPU的任务是各有偏重的,CPU主要用于基本数学和逻辑计 ### Flutter UI系统 -我们可以看到,无论是Android SDK还是iOS的UIKit 的职责都是相同的,它们只是语言载体和操作系统不同而已。那么可不可以实现这么一个UI系统:可以使用同一种编程语言开发,然后针对不同操作系统API抽象一个对上接口一致、对下适配不同操作系统的的中间层,然后在打包编译时再使用相应的中间层代码?如果可以做到,那么我们就可以使用同一套代码编写跨平台的应用了。而Flutter的原理正是如此,它提供了一套Dart API,然后再底层通过OpenGL这种跨平台的绘制库(内部会调用操作系统API)实现了一套代码跨多端。由于Dart API也是调用操作系统API,所以它的性能接近原生。 +我们可以看到,无论是Android SDK还是iOS的UIKit 的职责都是相同的,它们只是语言载体和底层的系统不同而已。那么可不可以实现这么一个UI系统:可以使用同一种编程语言开发,然后针对不同操作系统API抽象一个对上接口一致,对下适配不同操作系统的的中间层,然后在打包编译时再使用相应的中间层代码?如果可以做到,那么我们就可以使用同一套代码编写跨平台的应用了。而Flutter的原理正是如此,它提供了一套Dart API,然后在底层通过OpenGL这种跨平台的绘制库(内部会调用操作系统API)实现了一套代码跨多端。由于Dart API也是调用操作系统API,所以它的性能接近原生。 > 注意,虽然Dart是先调用了OpenGL,OpenGL才会调用操作系统API,但是这仍然是原生渲染,因为OpenGL只是操作系统API的一个封装库,它并不像WebView渲染那样需要JavaScript运行环境和CSS渲染器,所以不会有性能损失。 -至此,我们已经介绍了Flutter UI系统和操作系统交互的这一部分原理,先在需要说一些他对应用开发者定义的开发标准。其实在前面的章节中,我们已经对这个标准非常熟悉了, 简单概括就是:组合和响应式。我们要开发一个UI界面,需要通过组合其它Widget来实现,Flutter中,一切都是Widget,当UI要发生变化时,我们不去直接修改DOM,而是通过更新状态,让Flutter UI系统来根据新的状态来重新构建UI。 +至此,我们已经介绍了Flutter UI系统和操作系统交互的这一部分原理,现在需要说一些它对应用开发者定义的开发标准。其实在前面的章节中,我们已经对这个标准非常熟悉了, 简单概括就是:组合和响应式。我们要开发一个UI界面,需要通过组合其它Widget来实现,Flutter中,一切都是Widget,当UI要发生变化时,我们不去直接修改DOM,而是通过更新状态,让Flutter UI系统来根据新的状态来重新构建UI。 讲到这里,读者可能发现Flutter UI系统和Flutter Framework的概念是差不多的,的确如此,之所以用“UI系统”,是因为其他平台中可能不这么叫,我们只是为了概念统一,便于描述,读者不必纠结于概念本身。 diff --git a/docs/chapter14/render_object.md b/docs/chapter14/render_object.md index 57c39dac..3a34f406 100644 --- a/docs/chapter14/render_object.md +++ b/docs/chapter14/render_object.md @@ -1,10 +1,10 @@ # RenderObject和RenderBox -在上一节我们说过没给Element都对应一个RenderObject,我们可以通过`Element.renderObject` 来获取。并且我们也说过RenderObject的主要职责是Layout和绘制,所有的RenderObject会组成一棵渲染树Render Tree。本节我们将重点介绍一下RenderObject的作用。 +在上一节我们说过每个Element都对应一个RenderObject,我们可以通过`Element.renderObject` 来获取。并且我们也说过RenderObject的主要职责是Layout和绘制,所有的RenderObject会组成一棵渲染树Render Tree。本节我们将重点介绍一下RenderObject的作用。 -RenderObject就是渲染树种的一个对象,它拥有一个`parent`和一个`parentData` 插槽(slot),所谓插槽,就是指预留的一个接口或位置,这个接口和位置是由其它对象来接入或占据的,这个接口或位置在软件中通常用预留变量来表示,而`parentData`正是一个预留变量,它正是由`parent` 来赋值的,`parent`通常会通过子RenderObject的`parentData`存储一些和子元素相关的数据,如在Stack布局中,RenderStack就会将子元素的偏移数据存储在子元素的`parentData`中(具体可以查看Positioned实现)。 +RenderObject就是渲染树中的一个对象,它拥有一个`parent`和一个`parentData` 插槽(slot),所谓插槽,就是指预留的一个接口或位置,这个接口和位置是由其它对象来接入或占据的,这个接口或位置在软件中通常用预留变量来表示,而`parentData`正是一个预留变量,它正是由`parent` 来赋值的,`parent`通常会通过子RenderObject的`parentData`存储一些和子元素相关的数据,如在Stack布局中,RenderStack就会将子元素的偏移数据存储在子元素的`parentData`中(具体可以查看Positioned实现)。 -RenderObject类本身实现了一套基础的layout和绘制协议,但是并没有定义子节点模型(如一个节点可以有几个子节点,没有子节点?一个?两个?或者更多?)。 它也没有定义坐标系统(如子节点定位是在笛卡尔坐标中还是极坐标?)和具体的布局协议(是通过宽高还是通过constraint和size?,或者是否由父节点在子节点布局之前或之后设置子节点的大小和位置扥等)。为此,Flutter提供了一个RenderBox类,它继承自RenderObject,布局坐标系统采用笛卡尔坐标系,这和Android和iOS原生坐标系是一致的,都是屏幕的top、left是原点,然后分宽高两个轴,大多数情况下,我们直接使用RenderBox就可以了,除非遇到要自定义布局模型或坐标系统的情况,下面我们重点介绍一下RenderBox。 +RenderObject类本身实现了一套基础的layout和绘制协议,但是并没有定义子节点模型(如一个节点可以有几个子节点,没有子节点?一个?两个?或者更多?)。 它也没有定义坐标系统(如子节点定位是在笛卡尔坐标中还是极坐标?)和具体的布局协议(是通过宽高还是通过constraint和size?,或者是否由父节点在子节点布局之前或之后设置子节点的大小和位置等)。为此,Flutter提供了一个RenderBox类,它继承自RenderObject,布局坐标系统采用笛卡尔坐标系,这和Android和iOS原生坐标系是一致的,都是屏幕的top、left是原点,然后分宽高两个轴,大多数情况下,我们直接使用RenderBox就可以了,除非遇到要自定义布局模型或坐标系统的情况,下面我们重点介绍一下RenderBox。 ## 布局过程 @@ -34,7 +34,7 @@ void layout(Constraints constraints, { bool parentUsesSize = false }) { } ``` -可以看到`layout`方法需要传入两个参数,第一个为constraints,即 父节点对子节点大小的限制,该值根据父节点的布局逻辑确定。另外一个参数是 parentUsesSize,该值用于确定 `relayoutBoundary`,该参数表示子节点布局变化是否影响父节点,如果为`true`,当子节点布局发生变化时父节点都会标记为需要重新布局,如果为`false`,则子节点布局发生变化后则不会影响父节点。 +可以看到`layout`方法需要传入两个参数,第一个为constraints,即 父节点对子节点大小的限制,该值根据父节点的布局逻辑确定。另外一个参数是 parentUsesSize,该值用于确定 `relayoutBoundary`,该参数表示子节点布局变化是否影响父节点,如果为`true`,当子节点布局发生变化时父节点都会标记为需要重新布局,如果为`false`,则子节点布局发生变化后不会影响父节点。 #### relayoutBoundary @@ -61,7 +61,7 @@ void markNeedsLayout() { #### performResize 和 performLayout -RenderBox实际的测量和布局逻辑是在`performResize()` 和 `performLayout()`两个方法中,RenderBox子类需要实现他们来定制自身的布局逻辑。根据`layout()` 源码可以看出只有 `sizedByParent` 为 `true` 时,`performResize()` 才会被调用,而 `performLayout()` 是每次布局都会被调用的。`sizedByParent` 意为该节点的大小是否仅通过 parent 传给它的 constraints 就可以确定了,即该节点的大小与它自身的属性和其子节点无关,比如如果一个控件永远充满 parent 的大小,那么 `sizedByParent `就应该返回` true`,此时其大小在 `performResize()` 中就确定了,在后面的 `performLayout()` 方法中将不会再被修改了,这种情况下 `performLayout()` 只负责布局子节点。 +RenderBox实际的测量和布局逻辑是在`performResize()` 和 `performLayout()`两个方法中,RenderBox子类需要实现这两个方法来定制自身的布局逻辑。根据`layout()` 源码可以看出只有 `sizedByParent` 为 `true` 时,`performResize()` 才会被调用,而 `performLayout()` 是每次布局都会被调用的。`sizedByParent` 意为该节点的大小是否仅通过 parent 传给它的 constraints 就可以确定了,即该节点的大小与它自身的属性和其子节点无关,比如如果一个控件永远充满 parent 的大小,那么 `sizedByParent `就应该返回` true`,此时其大小在 `performResize()` 中就确定了,在后面的 `performLayout()` 方法中将不会再被修改了,这种情况下 `performLayout()` 只负责布局子节点。 在 `performLayout()` 方法中除了完成自身布局,也必须完成子节点的布局,这是因为只有父子节点全部完成后布局流程才算真正完成。所以最终的调用栈将会变成:*layout() > performResize()/performLayout() > child.layout() > ...* ,如此递归完成整个UI的布局。 @@ -97,7 +97,7 @@ class BoxParentData extends ParentData { > 一定要注意,RenderObject的parentData 只能通过父元素设置. -当然,ParentData并不仅仅可以用来存储偏移信息,通常所有和子节点特定的数据都可以存储到子节点的ParentData中,如ContainerBoxParentData就保存了指向兄弟节点的`previousSibling`和`nextSibling`,`Element.visitChildren()`方法也正是通过它们来实现对子节点的遍历。再比如`KeepAlive` Widget,它使用KeepAliveParentDataMixin(继承自ParentData) 来保存子节的`keepAlive`状态。 +当然,ParentData并不仅仅可以用来存储偏移信息,通常所有和子节点特定的数据都可以存储到子节点的ParentData中,如ContainerBox的ParentData就保存了指向兄弟节点的`previousSibling`和`nextSibling`,`Element.visitChildren()`方法也正是通过它们来实现对子节点的遍历。再比如`KeepAlive` Widget,它使用KeepAliveParentDataMixin(继承自ParentData) 来保存子节的`keepAlive`状态。 ## 绘制过程 @@ -109,7 +109,7 @@ void paint(PaintingContext context, Offset offset) { } 通过context.canvas可以取到Canvas对象,接下来就可以调用Canvas API来实现具体的绘制逻辑。 -如果节点有子节点,它除了自身绘制逻辑之外,还要调用子节点的绘制方法。我们以RenderFlex对象为例说明: +如果节点有子节点,它除了完成自身绘制逻辑之外,还要调用子节点的绘制方法。我们以RenderFlex对象为例说明: ```dart @override @@ -161,7 +161,7 @@ void defaultPaint(PaintingContext context, Offset offset) { } ``` -很明显,由于Flex本身没有需要绘制的东西,所以直接遍历其子节点,然后调用`paintChild()`来绘制子节点,同时将子节点ParentData中再layout阶段保存的offset加上自身偏移作为第二个参数传递给`paintChild()`。而如果子节点如果还有子节点时,`paintChild()`方法还会调用子节点的`paint()`方法,如此递归完成整个节点树的绘制,最终调用栈为: *paint() > paintChild() > paint() ...* 。 +很明显,由于Flex本身没有需要绘制的东西,所以直接遍历其子节点,然后调用`paintChild()`来绘制子节点,同时将子节点ParentData中在layout阶段保存的offset加上自身偏移作为第二个参数传递给`paintChild()`。而如果子节点还有子节点时,`paintChild()`方法还会调用子节点的`paint()`方法,如此递归完成整个节点树的绘制,最终调用栈为: *paint() > paintChild() > paint() ...* 。 当需要绘制的内容大小溢出当前空间时,将会执行`paintOverflowIndicator()` 来绘制溢出部分提示,这个就是我们经常看到的溢出提示,如: diff --git a/docs/chapter2/thread_model_and_error_report.md b/docs/chapter2/thread_model_and_error_report.md index 8391a2ca..08985f5d 100644 --- a/docs/chapter2/thread_model_and_error_report.md +++ b/docs/chapter2/thread_model_and_error_report.md @@ -20,11 +20,11 @@ Dart 在单线程中是以消息循环机制来运行的,其中包含两个任 ## Flutter异常捕获 -Dart中可以通过`try/catch/finally`来捕获代码块异常,这个和其它变成语言类似,,如果读者不清楚,可以查看Dart语言文档,不在赘述,下面我们看看Flutter中的异常捕获。 +Dart中可以通过`try/catch/finally`来捕获代码块异常,这个和其它编程语言类似,,如果读者不清楚,可以查看Dart语言文档,不在赘述,下面我们看看Flutter中的异常捕获。 ### Flutter框架异常捕获 -Flutter 框架为我们在很多关键的方法进行了异常捕获。这里举一个例子,当我们布局发生越界或不和规范时,Flutter就会自动弹出一个错误界面,这是因为Flutter已经在执行build方法时添加了异常捕获,最终的源码如下: +Flutter 框架为我们在很多关键的方法进行了异常捕获。这里举一个例子,当我们布局发生越界或不合规范时,Flutter就会自动弹出一个错误界面,这是因为Flutter已经在执行build方法时添加了异常捕获,最终的源码如下: ```dart @override @@ -114,7 +114,7 @@ R runZoned(R body(), { - zoneSpecification:Zone的一些配置,可以自定义一些代码行为,比如拦截日志输出行为等,举个例子: - 下面面是拦截应用中所有调用`print`输出日志的行为。 + 下面是拦截应用中所有调用`print`输出日志的行为。 ```dart main() { diff --git a/docs/chapter3/img_and_icon.md b/docs/chapter3/img_and_icon.md index 27ad79b3..4b9cab3c 100644 --- a/docs/chapter3/img_and_icon.md +++ b/docs/chapter3/img_and_icon.md @@ -132,7 +132,7 @@ const Image({ ### ICON -Flutter中,可以像web开发一样使用iconfont,iconfont即“字体图标”,它是将图标做成字体文件,然后通过制定不同的字符而显示不同的图片。 +Flutter中,可以像web开发一样使用iconfont,iconfont即“字体图标”,它是将图标做成字体文件,然后通过指定不同的字符而显示不同的图片。 > 在字体文件中,每一个字符都对应一个位码,而每一个位码对应一个显示字形,不同的字体就是指字形不同,即字符对应的字形是不同的。而在iconfont中,只是将位码对应的字形做成了图标,所以不同的字符最终就会渲染成不同的图标。 diff --git a/docs/chapter3/text.md b/docs/chapter3/text.md index 5d46b7fd..2d6f85a1 100644 --- a/docs/chapter3/text.md +++ b/docs/chapter3/text.md @@ -196,7 +196,7 @@ const textStyle = const TextStyle( 如果在package包内部使用它自己定义的字体,也应该在创建文本样式时指定`package`参数,如上例所示。 -一个包也可以只提供字体文件而不需要在pubspec.yaml中声明。 这些文件应该包的`lib/`文件夹中。字体文件不会自动绑定到应用程序中,应用程序可以在声明字体时有选择地使用这些字体。假设一个名为my_package的包中有一个字体文件: +一个包也可以只提供字体文件而不需要在pubspec.yaml中声明。 这些文件应该存放在包的`lib/`文件夹中。字体文件不会自动绑定到应用程序中,应用程序可以在声明字体时有选择地使用这些字体。假设一个名为my_package的包中有一个字体文件: ``` lib/fonts/Raleway-Medium.ttf diff --git a/docs/chapter6/gridview.md b/docs/chapter6/gridview.md index a689fec8..61d5f86a 100644 --- a/docs/chapter6/gridview.md +++ b/docs/chapter6/gridview.md @@ -23,7 +23,7 @@ GridView({ ### SliverGridDelegateWithFixedCrossAxisCount -该子类实现了一个纵轴为固定数量子元素的layout算法,其构造函数为: +该子类实现了一个横轴为固定数量子元素的layout算法,其构造函数为: ```dart SliverGridDelegateWithFixedCrossAxisCount({ @@ -34,10 +34,10 @@ SliverGridDelegateWithFixedCrossAxisCount({ }) ``` -- crossAxisCount:纵轴子元素的数量。此属性值确定后子元素在纵轴的长度就确定了,即ViewPort纵轴长度/crossAxisCount。 +- crossAxisCount:横轴子元素的数量。此属性值确定后子元素在横轴的长度就确定了,即ViewPort横轴长度/crossAxisCount。 - mainAxisSpacing:主轴方向的间距。 -- crossAxisSpacing:纵轴方向子元素的间距。 -- childAspectRatio:子元素在纵轴长度和主轴长度的比例。由于crossAxisCount指定后子元素纵轴长度就确定了,然后通过此参数值就可以确定子元素在主轴的长度。 +- crossAxisSpacing:横轴方向子元素的间距。 +- childAspectRatio:子元素在横轴长度和主轴长度的比例。由于crossAxisCount指定后子元素横轴长度就确定了,然后通过此参数值就可以确定子元素在主轴的长度。 可以发现,子元素的大小是通过crossAxisCount和childAspectRatio两个参数共同决定的。注意,这里的子元素指的是子widget的最大显示空间,注意确保子widget的实际大小不要超出子元素的空间。 @@ -46,7 +46,7 @@ SliverGridDelegateWithFixedCrossAxisCount({ ```dart GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, //纵轴三个子widget + crossAxisCount: 3, //横轴三个子widget childAspectRatio: 1.0 //宽高比为1时,子widget ), children:[ @@ -66,7 +66,7 @@ GridView( #### GridView.count -GridView.count构造函数内部使用了SliverGridDelegateWithFixedCrossAxisCount,我们通过它可以快速的创建纵轴固定数量子元素的GridView,上面的示例代码等价于: +GridView.count构造函数内部使用了SliverGridDelegateWithFixedCrossAxisCount,我们通过它可以快速的创建横轴固定数量子元素的GridView,上面的示例代码等价于: ```dart GridView.count( @@ -87,7 +87,7 @@ GridView.count( ### SliverGridDelegateWithMaxCrossAxisExtent -该子类实现了一个纵轴子元素为固定最大长度的layout算法,其构造函数为: +该子类实现了一个横轴子元素为固定最大长度的layout算法,其构造函数为: ```dart SliverGridDelegateWithMaxCrossAxisExtent({ @@ -98,7 +98,7 @@ SliverGridDelegateWithMaxCrossAxisExtent({ }) ``` -maxCrossAxisExtent为子元素在纵轴上的最大长度,之所以是“最大”长度,是**因为纵轴方向每个子元素的长度仍然是等分的**,举个例子,如果ViewPort的纵轴长度是450,那么当maxCrossAxisExtent的值在区间(450/4,450/3]内的话,子元素最终实际长度都为150,而`childAspectRatio`所指的子元素纵轴和主轴的长度比为**最终的长度比**。其它参数和SliverGridDelegateWithFixedCrossAxisCount相同。 +maxCrossAxisExtent为子元素在横轴上的最大长度,之所以是“最大”长度,是**因为横轴方向每个子元素的长度仍然是等分的**,举个例子,如果ViewPort的横轴长度是450,那么当maxCrossAxisExtent的值在区间(450/4,450/3]内的话,子元素最终实际长度都为150,而`childAspectRatio`所指的子元素横轴和主轴的长度比为**最终的长度比**。其它参数和SliverGridDelegateWithFixedCrossAxisCount相同。 下面我们看一个例子: diff --git a/docs/chapter7/inherited_widget.md b/docs/chapter7/inherited_widget.md index 315e14a5..74804968 100644 --- a/docs/chapter7/inherited_widget.md +++ b/docs/chapter7/inherited_widget.md @@ -118,5 +118,5 @@ I/flutter ( 8513): Dependencies change #### 应该在didChangeDependencies()中做什么? -一般来说,子widget很少会重写此方法,因为在依赖改变后framework也都会调用`build()`方法。但是,如果你需要在依赖改变后执行一些昂贵的操作,比如网络请求,这时最好的方式就是在此方法中执行,这样可以每次`build()`都执行一次。 +一般来说,子widget很少会重写此方法,因为在依赖改变后framework也都会调用`build()`方法。但是,如果你需要在依赖改变后执行一些昂贵的操作,比如网络请求,这时最好的方式就是在此方法中执行,这样可以避免每次`build()`都执行这些昂贵操作。 diff --git a/docs/chapter9/index.md b/docs/chapter9/index.md index 2b0112c0..5fdfc840 100644 --- a/docs/chapter9/index.md +++ b/docs/chapter9/index.md @@ -1,6 +1,6 @@ ## 简介 -精心设计的动画会让用户界面感觉更直观、流畅,能改善用户体验。 Flutter可以轻松实现各种动画类型,对于许多widget,特别是[Material Design widgets](https://flutter.io/docs/reference/widgets/material),都带有在其设计规范中定义的标准动画效果(但也可以自定义这些效果)。本章将详细介绍Flutter的动画系统,并会通过几个小实例来演示,以帮助开发者可以迅速理解并掌握动画的开发流程与原理。 +精心设计的动画会让用户界面感觉更直观、流畅,能改善用户体验。 Flutter可以轻松实现各种动画类型,对于许多widget,特别是[Material Design widgets](https://flutter.io/docs/reference/widgets/material),都带有在其设计规范中定义的标准动画效果(但也可以自定义这些效果)。本章将详细介绍Flutter的动画系统,并会通过几个小实例来演示,以帮助开发者迅速理解并掌握动画的开发流程与原理。 ## 本章目录