大神教你举一反三---用实战讲述Flutter-跨平台框架应用
外链图片转存中…(img-ZqL8ukw1-1710946354908)]
当 isRepaintBoundary 为 true 时,该区域就是一个可更新绘制区域,而当这个区域形成时,就会新创建一个 Layer 。 但不是每个 RenderObject 都会有 Layer , 因为这受 isRepaintBoundary 的影响。


注意,Flutter 中常见的
BuildContext,其实就是Element的抽象,通过BuildContext,我们一般情况就可以对应获得Element,也就是拿到了“仓库的钥匙” ,通过context就可以去获取Element内持有的东西,比如前面所说的RenderObject,还有后面我们会谈到State等。
1.2 Widget 的分类
这里我们将 Widget 分为如下图所示分类:是否存在 State 、是否存在RenderObject 。

其实还可以按照
RenderBox和RenderSliver分类,但是篇幅原因以后再介绍。
1.2.1 是否存在 State
Flutter 中我们常用的 Widget 有: StatelessWidget 和 StatefulWidget 。
如下图, StatelessWidget 的代码很简单,因为 Widget 是不可变的,传入的 text 决定了它显示的内容,并且 text 也算是 final 的。

注意图中
DemoPage有个黄色警告,这是因为我们定义了int i = 0不是 final 导致的,在StatelessWidget中, 非 final 的变量起始容易产生误解,因为Widget本事就是不可变的。
前面我们说过 Widget 都是不可变的,在这个基础上, StatefulWidget 的 State ,帮我们实现了 Widget 的跨帧绘制 ,也就是在每次 Widget 重构时,可以通过 State 重新赋予 Widget 需要的配置信息,而这里的 State 对象,就是存在每个 Element 里的。
同时,前面我们说过,Flutter 内的
BuildContext其实就是Element的抽象,这说明我们可以通过context去获取Element内的东西,比如State、RenderObject、Widget。
Widget ancestorWidgetOfExactType
State ancestorStateOfType
State rootAncestorStateOfType
RenderObject ancestorRenderObjectOfType
如下图所示,保存在 State 中的 text ,当我们点击按键时,setState 时它被标志为 "变化了" , 它可以主动发生改变,保存变量,不再只是“只读”状态了。

1.2.2、容器 Widget/渲染 Widget
在 Flutter 中还有 容器 Widget 和 渲染Widget 的区别,一般情况下:
-
Text、Slider、ListTile等都是属于渲染Widget,其内部主要是RenderObjectElement,对应有RenderObject参数。 -
StatelessWidget/StatefulWidget等属于容器Widget,其内部使用的是ComponentElement,ComponentElement本身是不存在RenderObject的。
所以作为容器 Widget, 获取它们的 RenderObject 时,获取到的是 build 后的树结构里,最上面渲染 Widget的 RenderObject 。

如上图所示
findRenderObject的实现,最终就是获取renderObject,在遇到ComponentElement时,执行的是element.visitChildren(visit);, 递归直到找到RenderObjectElement,再返回它的renderObject。
获取 RenderObject 在 Flutter 里很重要的,因为获取控件的位置和大小等,都需要通过 RenderObject 获取。
1.3、RenderObject
Flutter 中各类 RenderObject 的实现,大多都是颗粒度很细,功能很单一的存在 :

然而接触过 Flutter 的同学应该知道 Container 这个 Widget ,Container 的功能却不显单一,这是为什么呢?
如下图,因为 Container 其实是容器 Widget ,它只是把其他“单一”的 Widget 做了二次封装,然后通过配置参数来达到 “多功能的效果” 而已。

所以 Flutter 开发中,我们经常会根据功能定义出各类如 Continer、Scaffold 等脚手架模版,实现灵活与复用的界面开发。
回归到 RenderObject ,事实上 RenderObject 还属于比较“低级”的阶段,因为绘制到屏幕上我们还需要坐标体系和布局协议等,所以 大部分 Widget 的 RenderObject 会是子类 RenderBox (RenderSliver 例外) ,因为 RenderObject 本身只实现了基础的 layout 和 paint ,而绘制到屏幕上,我们需要的坐标和大小等,这些内容是在 RenderBox 中开始实现。
RenderSliver主要是在滚动控件中继承使用。
比如控件被绘制在 x=10,y=20 的位置,然后大小由 parent 对它进行约束显示,RenderBox 继承了 RenderObject,在其基础上实现了 笛卡尔坐标系 和布局协议。
这里我们通过 Offstage 这个 Widget ,看下其 RenderBox 子类的实现逻辑, Offstage 是用于控制 child 是否显示的作用,如下图,可以看到 RenderOffstage 对于 offstage 标志位的内部逻辑:
[图片上传失败…(image-6da7e5-1571570507182)]
那么 Flutter 中的布局协议是什么呢?
简单来说就是 child 和 parent 之间的大小应该怎么显示,由谁决定显示区域。 相信从 Android 到接触 Flutter 的同学有这样的疑惑, Flutter 中的 match_parent 和 wrap_content 逻辑需要怎么设置?
就我们从一个简单的代码分析,如下图所示,Row 布局我们没有设置任何大小,它是怎么确定自身大小的呢?

我们翻阅源码,可以发现其实 Flutter 中常用的 Row 、Column 等其实都是 Flex 的子类,只是对 Flex 做了简单默认配置。

那按照我们前面的理解,看一个 Widget 的实现逻辑,就应该看它的 RenderObject ,而在 Flex 布对应的 RenderFlex 中,我们可以看到如下一段代码:

可以看到在布局的时候,RenderFlex 首先要求 constraints != null ,Flex 布局的上层中必须存在约束,不然肯定会报错。
之后,在布局时,Row 布局的 direction 是横向的,所以 maxMainSize 为上层布局的最大宽度,然后根据我们配置的 mainAxisSize 的参数:
- 当
mainAxisSize为max时,我们Row的横向布局就是maxMainSize; - 当
mainAxisSize为min时,我们Row的横向布局就是allocatedSize;
前面 maxMainSize 我们知道了是父布局的最大宽度,而 allocatedSize 其实就是 child 的宽度之和。所以结果很明显了:
对于 Row 来说, mainAxisSize 为 max 时就是 match_parent ;mainAxisSize 为 min 时就是 wrap_content 。
而高度 crossSize ,其实是由 math.max(crossSize, _getCrossSize(child)); 决定,也就是 child 中最高的一个作为其高度。
最后小结一个知识点:
布局一般都是由上层往下传递 Constraints ,然后由下往上返回 Size。

那如何直接自定义 RenderObject 布局?
抛开 Flutter 为我们封装的好的,三大金刚 Widget 、Element 、RednerObject 一个不少,当然, Flutter 内置了很多封装帮我们节省代码。
一般情况下自定义 RenderObject 布局:
- 我们会继承
MultiChildRenderObjectWidget和RenderBox这两个abstract类,实现自己的Widget和RenderObject对象; - 然后利用
MultiChildRenderObjectElement关联起它们; - 除此之外,还有几个关键的类:
ContainerRenderObjectMixin、RenderBoxContainerDefaultsMixin和ContainerBoxParentData等可以帮你减少代码量。

总结起来, 对于 Flutter 而言,整个屏幕都是一块画布,我们通过各种 Offset 和 Rect 确定了位置,然后通过 Canvas 绘制上去,目标是整个屏幕区域,整个屏幕就是一帧,每次改变都是重新绘制。
这里没有介绍
RenderSliver相关,它的输入和输出和Renderbox又不大一样,有机会我们后面再详细介绍。
三、Flutter 的实战技巧
3.1、InheritedWidget
InheritedWidget 是 Flutter 的灵魂设定之一。
InheritedWidget 共享的是 Widget ,只是这个 Widget 是一个 ProxyWidget ,它自己本身并不绘制什么,但共享这个 Widget 内保存有的数据,从而到了共享状态的目的。
如下图所示,是 Flutter 中常见的 Theme ,其内部就是使用了 _InheritedTheme 这个 InheritedWidget 来实现主题的全局共享的。那么 InheritedWidget 是如何实现全局共享的呢?

其实在 Element 的内部有一个 Map<Type, InheritedElement> _inheritedWidgets; 参数,_inheritedWidgets 一般情况下是空的,只有当父控件是 InheritedWidget 或者本身是 InheritedWidget 时,它才会被初始化,而当父控件是 InheritedWidget 时,这个 Map 会被一级一级往下传递与合并。
所以当我们通过 context 调用 inheritFromWidgetOfExactType 时,就可以通过这个 Map 往上查找,从而找到这个上级的 InheritedWidget 。(毕竟 context is Element)

如我们的 Theme/ThemeData 、Text/DefaultTextStyle、Slider / SliderTheme 等,如下代码所示,我们可以定义全局的 ThemeData 或者局部的 DefaultTextStyle ,从而实现全局的自定义和局部的自定义共享等。


其实,Flutter 中大部分的状态管理控件,其状态共享方法,也是基于
InheritedWidget去实现的。
3.2、支持原生控件
前面我们说过, Flutter 既然不依赖于原生控件,那么如何集成一些平台已有的控件呢?比如 WebView 和 Map ?
我们这里以 WebView 为例子:
在官方 WebView 控件支持出来之前 ,第三方是直接在 FlutterView 上覆盖了一个新的原生控件,利用 Dart 中的占位控件传递位置和大小。
如下图,在 Flutter 端 push 出一个 设定好位置和大小 的 SingleChildRenderObjectWidget ,从而得到需要显示的大小和位置,将这些信息通过 MethodChannel 传递到原生层,在原生层 addContentView 一个指定大小和位置的 WebView 。
这时候 WebView 和 SingleChildRenderObjectWidget 处于一样的大小和位置,而空白部分则用 FLutter 的 Appbar 显示。

这样看起来就像是在 Flutter 中添加了 WebView ,但实际这脱离了 Flutter 的渲染树,其中一个问题就是,当你跳转 Flutter 其他页面的时候,会被 WebView 挡住;并且打开页面的动画,Appbar 和 WebView 难以保持一致。

后面 官方 WebView 控件支持出来后,这时候官方是利用 PlatformView 的设计,完成了不脱离 Flutter 渲染堆栈,也能集成平台原生控件的功能。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。



由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
]
[外链图片转存中…(img-ZqL8ukw1-1710946354908)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-cJuPQ3mX-1710946354908)]
更多推荐
所有评论(0)