Widget 分类
如果按照是否是有状态的分类方式,那么Widget
就分为StatelessWidget
和StatefulWidget
,StatelessWidget
和StatefulWidget
的Element
都是ComponentElement
,并且都不具备RenderObject
。
他们UI
的构建都是调用build
方法。区别就是StatelessWidget
只是简单的实现了ComponentElement
,而StatefulWidget
则复杂了许多,他的build
是由_state
去控制的,状态和数据都保存在这里面, 这个在之前的文章中有提及。
StatelessElement
代码示例:
class StatelessElement extends ComponentElement {
/// Creates an element that uses the given widget as its configuration.
StatelessElement(StatelessWidget widget) : super(widget);
@override
StatelessWidget get widget => super.widget as StatelessWidget;
@override
Widget build() => widget.build(this);
@override
void update(StatelessWidget newWidget) {
super.update(newWidget);
assert(widget == newWidget);
_dirty = true;
rebuild();
}
}
可以看出在更新的时候也只是把_dirty脏标记设置为true,然后就重新构建。
State和Element的生命周期对比
![](https://img.haomeiwen.com/i1940927/4f8aa84d39f478d3.png)
State数据的传递
![](https://img.haomeiwen.com/i1940927/315b211af6c6be0c.png)
上面的代码,当点击FloatingActionButton
之后,最终显示在屏幕上的文字是什么?为什么?
答案:
上面的代码当我们点击按钮之后,内容并不会发生改变,因为StatePage
的state
已经被创建过了,所以createState
不会走两次,故而data并不会发生改变(但是StatePage
的data
是发生了改变的),如果我们想使用更新之后的值,我们可以使用widte.data
来引用。
class StatePage extends StatefulWidget {
StatePage({this.data});
final String data;
@override
_StatePageState createState() => _StatePageState();
}
class _StatePageState extends State<StatePage> {
@override
Widget build(BuildContext context) {
return Center(
child: Text(widget.data ?? ""),
);
}
}
setState是如何实现刷新的?
setState
内部会调用_element.markNeedsBuild();
方法markNeedsBuild
方法会在内部把_dirty
设置为true
,然后加入到定时器当中,然后在下一帧的WidgetsBinding.drawFrame
才会被绘制。此处也可以得知,setState
并不会马上生效。
RenderObject分类
RenderBox
特性:会根据parent
的constraints
大小判断自己的布局方法,然后将constraints
传递给child
得到child
的大小,最后根据child
返回的Size
决定自己的Size
,如果没有child
,就使用自己的Size
。
他用于那些不涉及的滚动的控件布局,他的两个关键参数就是BoxConstraints
和Size
。
RenderSliver
特性:因为其主要用于RenderViewport之后,里面涉及的运算和属性对比RenderBox要复杂上许多。他的两个关键参数是SliverConstraints和SliverGeometry。
SliverConstraints和BoxConstraints对比,BoxContraints只包括了,最大/最小的高度/宽度。但是SliverConstraints则更多的是滑动方向、滑动偏移、滑动容器大小、容器缓存大小和位置等相关参数。
Size和SliverGeometry进行对比,Size只包括了宽和高。但是SliverGeometry包括了滑动方位、绘制范围、偏移等相关参数。
RenderBox和RenderSliver对比
RenderBox输入输出相较于RenderSliver更为简单,RenderSliver更为关注滑动、方向、缓存等关键点,这也是因为其需要和ViewPort配合展示。例如我们经常使用的ListView、GirdView、ScrollView等都是有Sliver和ViewPort组成的,可滑动的区域内不可以直接使用RenderBox,如果一定要使用必须用RenderSliver进行嵌套后进行布局。
![](https://img.haomeiwen.com/i1940927/2e876eb6b6ea31c0.png)
ViewPort
ViewPort
根据自己的窗口的大小和偏移量,对child
进行布局计算,通过对child
输入SliverConstraints
来得到child
的SliverGeometry
,从而确定layout
和paint
等相关信息。
RenderSliver
对应的Sliver
控件需要在ViewPort
中使用。
![](https://img.haomeiwen.com/i1940927/811e0cde60b2a2c4.png)
当外部的滑动事件产生时,就会触发到ViewPort
的markNeedsLayout
方法,之后变化重新进行布局和绘制,并让Sliver
在ViewPort
中进行偏移,达到看起来像是滑动了的效果。
RenderViewPort
中为了避免性能消耗,对于滑动的时候内部就会尝试重新布局做了一个限制,最大的尝试次数不能超过10次。
ListView
、GridView
内部都是一个SliverList
构成,他们的children
布局也是通过SliverList
进行布局的。
RenderSliverList
中,会通过传入的ramainingCacheExtent
、scrollOffset
等参数去决定哪些child
需要布局显示,哪些child
不需要被布局绘制,从而保证了列表中内存优化和良好的绘制性能。
单元素与多元素分类
根据Widget
的child
是否支持单个/多个child
又可以分为SingleChildRenderObjectWidget
和MultiChildRenderObjectWidget
。
像我们经常使用的Clip
、Opacity
、Padding
、Align
、SizededBox
等都属于SingleChildRenderObjectWidget
;而Stack
、Row
、Column
、RichText
等则属于MultiChildRenderObjectWidget
。针对两个不同的RenderObjectWidget
,Flutter
提供了CustomSingleChildLayout
和CustomMultiChildLayout
的抽象封装。
SingleChildRenderObjectWidget
SingleChildRenderObjectWidget
继承RenderObjectWidget
,因为只有一个child
,所以实现起来相对简单。绘制流程是通过RenderObject
计算出自身的最大、最小宽高,并且通过performLayout
综合得到child
返回的Size
、最后在进行绘制。
MultiChildRenderObjectWidget
![](https://img.haomeiwen.com/i1940927/fe4e94e7e9e25dbf.png)
从上图可以看出相较于SingleChildRenderObjectWidget
,MultiChildRenderObjectWidget
实现起来要复杂许多,主要复杂的部分在于RenderBox
,我们需要自定义一个类继承于RenderBox
,同时还得混入ContainerRenderObjectMixin
和RenderBoxContainerDefaultsMixin
,然后去重写他的两个方法:setupParentData
和performLayout
,然后在重写paint
方法,调用系统绘制方法,完成绘制操作。
下面用一个实际例子来演示:
01- 创建ContainerBoxparentData
这个就是对应上图中右下方的抽象类(ConstainerBoxParentData
)的具体实现
class RenderCloudParentData extends ContainerBoxParentData<RenderBox> {
/// 定义宽高
double width;
double height;
/// 通过offset和width、height得到一个矩形区域
Rect get content => Rect.fromLTWH(
offset.dx,
offset.dy,
width,
height,
);
}
02-创建RenderBox
这个就是对应上图的RenderBox
的具体实现
/// 从类的定义就可以很好的看出,该类需要继承于RenderBox,
/// 同时还需要混入ContainerRenderObjectMixin、RenderBoxContainerDefaultsMixin
class RenderCloudWidget extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, RenderCloudParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, RenderCloudParentData> {
/// 构造方法
/// * children
/// * overflow 裁剪方式
/// * ratio 比例
RenderCloudWidget({
List<RenderBox> children,
Clip overflow = Clip.none,
double ratio,
}) : _ratio = ratio,
_overflow = overflow {
/// 这个是ContainerRenderObjectMixin的内部方法,其内部是一个双线链表的结果,
/// 主要是用于快速定位下一个、上一个renderObject
addAll(children);
}
///圆周
double _mathPi = math.pi * 2;
///比例
double _ratio;
double get ratio => _ratio;
set ratio(double value) {
assert(value != null);
if (_ratio != value) {
_ratio = value;
markNeedsPaint();
}
}
/// 裁剪方式
Clip get overflow => _overflow;
set overflow(Clip value) {
assert(value != null);
if (_overflow != value) {
_overflow = value;
markNeedsPaint();
}
}
Clip _overflow;
/// 是否需要裁剪
bool _needClip = false;
/// 用于判断是否重复区域了
bool overlaps(RenderCloudParentData data) {
Rect rect = data.content;
RenderBox child = data.previousSibling;
if (child == null) {
return false;
}
do {
RenderCloudParentData childParentData = child.parentData;
if (rect.overlaps(childParentData.content)) {
return true;
}
child = childParentData.previousSibling;
} while (child != null);
return false;
}
/// 这个就是需要重写RenderBox其中的一个方法
@override
void setupParentData(covariant RenderObject child) {
if (child.parentData is! RenderCloudParentData) {
child.parentData = RenderCloudParentData();
}
}
/// 内部布局方法,布局每一个child的位置大小
@override
void performLayout() {
///默认不需要裁剪
_needClip = false;
///没有 childCount 不玩
if (childCount == 0) {
size = constraints.smallest;
return;
}
///初始化区域
var recordRect = Rect.zero;
var previousChildRect = Rect.zero;
RenderBox child = firstChild;
while (child != null) {
var curIndex = -1;
///提出数据
final RenderCloudParentData childParentData = child.parentData;
child.layout(constraints, parentUsesSize: true);
var childSize = child.size;
///记录大小
childParentData.width = childSize.width;
childParentData.height = childSize.height;
do {
///设置 xy 轴的比例
var rX = ratio >= 1 ? ratio : 1.0;
var rY = ratio <= 1 ? ratio : 1.0;
///调整位置
var step = 0.02 * _mathPi;
var rotation = 0.0;
var angle = curIndex * step;
var angleRadius = 5 + 5 * angle;
var x = rX * angleRadius * math.cos(angle + rotation);
var y = rY * angleRadius * math.sin(angle + rotation);
var position = Offset(x, y);
///计算得到绝对偏移
var childOffset = position - Alignment.center.alongSize(childSize);
++curIndex;
///设置为遏制
childParentData.offset = childOffset;
///判处是否交叠
} while (overlaps(childParentData));
///记录区域
previousChildRect = childParentData.content;
recordRect = recordRect.expandToInclude(previousChildRect);
///下一个
child = childParentData.nextSibling;
}
///调整布局大小
size = constraints
.tighten(
height: recordRect.height,
width: recordRect.width,
)
.smallest;
///居中
var contentCenter = size.center(Offset.zero);
var recordRectCenter = recordRect.center;
var transCenter = contentCenter - recordRectCenter;
child = firstChild;
while (child != null) {
final RenderCloudParentData childParentData = child.parentData;
childParentData.offset += transCenter;
child = childParentData.nextSibling;
}
///超过了嘛?
_needClip =
size.width < recordRect.width || size.height < recordRect.height;
}
/// 设置绘制默认
@override
void paint(PaintingContext context, Offset offset) {
if (!_needClip || _overflow == Clip.none) {
defaultPaint(context, offset);
} else {
context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
defaultPaint,
);
}
}
/// 触摸测试,如果不想响应就返回false,反正则是true
@override
bool hitTestChildren(HitTestResult result, {Offset position}) {
return defaultHitTestChildren(result, position: position);
}
}
03-创建Widget
主要是把RenderObject
和Widget
进行关联起来
/// 创建Widget,继承与MultiChildRenderObjectWidget
/// 主要是和之前的RenderBox关联起来
class CloudWidget extends MultiChildRenderObjectWidget {
/// 自定义的相关属性
final Clip overflow;
final double ratio;
/// 构造方法
CloudWidget({
Key key,
this.ratio = -1,
this.overflow = Clip.none,
List<Widget> children = const <Widget>[],
}) : super(key: key, children: children);
/// 重写创建RenderObject的方法,把之前创建的RenderCouldWidget返回
@override
RenderObject createRenderObject(BuildContext context) {
return RenderCloudWidget(ratio: ratio, overflow: overflow);
}
/// 在这里更新RenderCloudWidget的两个关键参数
@override
void updateRenderObject(
BuildContext context, covariant RenderCloudWidget renderObject) {
/// ..表示级联操作符
renderObject
..ratio = ratio
..overflow = overflow;
}
}
04-demo
///云词图
class CloudDemoPage extends StatefulWidget {
@override
_CloudDemoPageState createState() => _CloudDemoPageState();
}
class _CloudDemoPageState extends State<CloudDemoPage> {
///Item数据
List<CloudItemData> dataList = const <CloudItemData>[
CloudItemData('CloudGSY11111', Colors.amberAccent, 10, false),
CloudItemData('CloudGSY3333333T', Colors.limeAccent, 16, false),
CloudItemData('CloudGSYXXXXXXX', Colors.black, 14, true),
CloudItemData('CloudGSY55', Colors.black87, 33, false),
CloudItemData('CloudGSYAA', Colors.blueAccent, 15, false),
CloudItemData('CloudGSY44', Colors.indigoAccent, 16, false),
CloudItemData('CloudGSYBWWWWWW', Colors.deepOrange, 12, true),
CloudItemData('CloudGSY<<<', Colors.blue, 20, true),
CloudItemData('FFFFFFFFFFFFFF', Colors.blue, 12, false),
CloudItemData('BBBBBBBBBBB', Colors.deepPurpleAccent, 14, false),
CloudItemData('CloudGSY%%%%', Colors.orange, 20, true),
CloudItemData('CloudGSY%%%%%%%', Colors.blue, 12, false),
CloudItemData('CloudGSY&&&&', Colors.indigoAccent, 10, false),
CloudItemData('CloudGSYCCCC', Colors.yellow, 14, true),
CloudItemData('CloudGSY****', Colors.blueAccent, 13, false),
CloudItemData('CloudGSYRRRR', Colors.redAccent, 12, true),
CloudItemData('CloudGSYFFFFF', Colors.blue, 12, false),
CloudItemData('CloudGSYBBBBBBB', Colors.cyanAccent, 15, false),
CloudItemData('CloudGSY222222', Colors.blue, 16, false),
CloudItemData('CloudGSY1111111111111111', Colors.tealAccent, 19, false),
CloudItemData('CloudGSY####', Colors.black54, 12, false),
CloudItemData('CloudGSYFDWE', Colors.purpleAccent, 14, true),
CloudItemData('CloudGSY22222', Colors.indigoAccent, 19, false),
CloudItemData('CloudGSY44444', Colors.yellowAccent, 18, true),
CloudItemData('CloudGSY33333', Colors.lightBlueAccent, 17, false),
CloudItemData('CloudGSYXXXXXXXX', Colors.blue, 16, true),
CloudItemData('CloudGSYFFFFFFFF', Colors.black26, 14, false),
CloudItemData('CloudGSYZUuzzuuu', Colors.blue, 16, true),
CloudItemData('CloudGSYVVVVVVVVV', Colors.orange, 12, false),
CloudItemData('CloudGSY222223', Colors.black26, 13, true),
CloudItemData('CloudGSYGFD', Colors.yellow, 14, true),
CloudItemData('GGGGGGGGGG', Colors.deepPurpleAccent, 14, false),
CloudItemData('CloudGSYFFFFFF', Colors.blueAccent, 10, true),
CloudItemData('CloudGSY222', Colors.limeAccent, 12, false),
CloudItemData('CloudGSY6666', Colors.blue, 20, true),
CloudItemData('CloudGSY33333', Colors.teal, 14, false),
CloudItemData('YYYYYYYYYYYYYY', Colors.deepPurpleAccent, 14, false),
CloudItemData('CloudGSY 3 ', Colors.blue, 10, false),
CloudItemData('CloudGSYYYYYY', Colors.black54, 17, true),
CloudItemData('CloudGSYCC', Colors.lightBlueAccent, 11, false),
CloudItemData('CloudGSYGGGGG', Colors.deepPurpleAccent, 10, false)
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text("CloudDemoPage"),
),
body: new Center(
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.width,
///利用 FittedBox 约束 child
child: new FittedBox(
/// Cloud 布局
child: Container(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 6),
color: Colors.brown,
///布局
child: CloudWidget(
///容器宽高比例
ratio: 1,
children: <Widget>[
for (var item in dataList)
///判断是否旋转
RotatedBox(
quarterTurns: item.rotate ? 1 : 0,
child: Text(
item.text,
style: new TextStyle(
fontSize: item.size,
color: item.color,
),
),
),
],
),
),
),
),
),
);
}
}
class CloudItemData {
///文本
final String text;
///颜色
final Color color;
///旋转
final bool rotate;
///大小
final double size;
const CloudItemData(
this.text,
this.color,
this.size,
this.rotate,
);
}
CustomMultiChildLayout
官方为了简化我们实现自定义布局的方式,还提供了CustomMultiChildLayout
这样的类,这个类也是继承了MultiChildRenderObjectWidget
,并通过一个代理(MultiChildLayoutDelegate
)来完成自定义UI相关的功能,通过这个代理,我们可以直接去重写内部的performLayout
方法,从而达到我们自定布局的效果。
01-创建Delegate
class CircleLayoutDelegate extends MultiChildLayoutDelegate {
final List<String> customLayoutId;
final Offset center;
Size childSize;
CircleLayoutDelegate(
this.customLayoutId, {
this.center = Offset.zero,
this.childSize,
});
@override
void performLayout(Size size) {
for (var item in customLayoutId) {
if (hasChild(item)) {
double r = 100;
/// 下标
int index = int.parse(item);
/// 均分
double step = 360 / customLayoutId.length;
/// 角度
double hd = (2 * math.pi / 360) * step * index;
var x = center.dx + math.sin(hd) * r;
var y = center.dy + math.cos(hd) * r;
/// 使用??= 避免多次赋值
childSize ??= Size(size.width / customLayoutId.length,
size.height / customLayoutId.length);
layoutChild(item, BoxConstraints.loose(childSize));
final double centerX = childSize.width * 0.5;
final double centerY = childSize.height * 0.5;
var result = Offset(x - centerX, y - centerY);
/// 设置child位置
positionChild(item, result);
}
}
}
@override
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) {
return true;
}
}
02-使用
class CustomMultiLayoutPage extends StatefulWidget {
@override
_CustomMultiLayoutPageState createState() => _CustomMultiLayoutPageState();
}
class _CustomMultiLayoutPageState extends State<CustomMultiLayoutPage> {
///用于 LayoutId 指定
///CircleLayoutDelegate 操作具体 Child 的 ChildId 是通过 LayoutId 指定的
List customLayoutId = ["0", "1", "2", "3", "4"].toList();
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final childSize = 66.0;
return Scaffold(
appBar: AppBar(),
body: Center(
child: Container(
color: Colors.yellowAccent,
width: size.width,
height: size.width,
child: CustomMultiChildLayout(
delegate: CircleLayoutDelegate(
customLayoutId,
childSize: Size(childSize, childSize),
center: Offset(size.width * 0.5, size.width * 0.5),
),
children: [
///使用 LayoutId 指定 childId
for (var item in customLayoutId)
new LayoutId(id: item, child: ContentItem(item, childSize)),
],
),
),
),
persistentFooterButtons: <Widget>[
TextButton(onPressed: () {
setState(() {
customLayoutId.add("${customLayoutId.length}");
});
},
child: Icon(Icons.add),
),
TextButton(onPressed: () {
setState(() {
if (customLayoutId.length > 1) {
customLayoutId.removeLast();
}
});
},
child: Icon(Icons.remove),
),
],
);
}
}
class ContentItem extends StatelessWidget {
final String text;
final double childSize;
ContentItem(this.text, this.childSize);
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(childSize / 2.0),
child: InkWell(
radius: childSize / 2.0,
customBorder: CircleBorder(),
onTap: () {},
child: Container(
width: childSize,
height: childSize,
child: Center(
child: Text(
text,
style: Theme.of(context)
.textTheme
.headline6
.copyWith(color: Colors.white),
),
),
),
),
);
}
}
效果图
![](https://img.haomeiwen.com/i1940927/3534393268c24ff2.gif)
InheritedWidget共享状态
InheritedWidge
是Flutter Widget
中非常重要的一个构成部分,因为InheritedWidget
常被用于数据共享。比如使用频率很高的:Theme/ThemeData
、Text/DefaultTextStyle
、Slider/SliderTheme
、Icon/IconTheme
等内部都是通过InheritedWidget
实现数据共享的。并且Flutter
中部分的状态管理框架,内部的状态共享方法也是基于InheritedWidget
去实现的。
InheritedWidget继承自ProxyWidget,本身并不具备绘制的能力,但共享这个Widget等与共享Widget内保存的数据,获取Widget就可以获取到其内部保存的数据,如下图:
![](https://img.haomeiwen.com/i1940927/99644563826705b2.png)
每一个Element
当中都有一个成员变量:Map<Type, InheritedElement> _inheritedWidgets
,改成员变量默认是空,之后当父控件是InheritedWidget
或者本身是InheritedWidget
的时候才会初始化,当父控件是InheritedWidget
的时候,这个Map
会逐级向下传递于合并。
那么context.inheritedFromWidgetOfExactType
内部做了啥呢?
通过查看Element
的源码截图部分片段
@override
InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
assert(_debugCheckStateIsActiveForAncestorLookup());
/// 首先判断是否有inheritedElement类型的数据
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
/// 找到了
if (ancestor != null) {
assert(ancestor is InheritedElement);
/// 添加到依赖集合中,并且通过updateDependencies将当前的Element添加到_dependencies Map中,并且返回InheritedWidget
return inheritFromElement(ancestor, aspect: aspect);
}
_hadUnsatisfiedDependencies = true;
return null;
}
/// 下面两个方法就是添加过程的实现
@override
InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
return dependOnInheritedElement(ancestor, aspect: aspect);
}
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
assert(ancestor != null);
/// 创建_dependencies
_dependencies ??= HashSet<InheritedElement>();
/// 添加InheritedElement到集合中
_dependencies.add(ancestor);
/// 跟新依赖
ancestor.updateDependencies(this, aspect);
/// 返回InheritedWidget
return ancestor.widget;
}
InheritedWidget是如何通知StatefulWidget进行更新的?
例如:当我们在外界调用Theme.of(context)
的时候,BuildContext
的实现就是Element
,所以当内部调用到context.inheritedFromWidgetOfExactType
时,就会将context
所代表的Element
添加到InheritedElement
的_dependents
中,当InheritedElement
被更新的时候,就会触发到齐内部的notifyClients
方法,该方法就会挨个遍历被加入到_dependents
,从而触发到didChangeDependcies
,然后就会更新UI
ErrorWidget 异常处理
在以往的开发中,当我们程序抛出一些未处理的异常或者错误的时候,就会引发程序的crash
,但是在Flutter
中则不会,这是因为Flutter
中有一个全局处理的地方;
当我们的代码发生一些问题之后,在debug
模式下可能会有某些或者整个页面变成红色,并显示一些错误信息;在release
模式下,则会显示灰色的并没有错误提示。
为了能让我们的产品体验更好,我们可以在main方法中做一些处理,让错误看起来更加优雅
void main() {
runZoned((){
ErrorWidget.builder = (FlutterErrorDetails details) {
Zone.current.handleUncaughtError(details.exception, details.stack);
return Container(color: Colors.orange,);
};
FlutterError.onError = (FlutterErrorDetails details) async {
FlutterError.dumpErrorToConsole(details);
Zone.current.handleUncaughtError(details.exception, details.stack);
};
runApp(MyApp());
}, onError: (Object obj, StackTrace stack) {
print(obj);
print(stack);
});
}
网友评论