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的生命周期对比
image-20210313144623647.pngState数据的传递
数据传递上面的代码,当点击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进行嵌套后进行布局。
对比ViewPort
ViewPort
根据自己的窗口的大小和偏移量,对child
进行布局计算,通过对child
输入SliverConstraints
来得到child
的SliverGeometry
,从而确定layout
和paint
等相关信息。
RenderSliver
对应的Sliver
控件需要在ViewPort
中使用。
当外部的滑动事件产生时,就会触发到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
MultiChildRenderObjectWidget从上图可以看出相较于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),
),
),
),
),
);
}
}
效果图
111.gifInheritedWidget共享状态
InheritedWidge
是Flutter Widget
中非常重要的一个构成部分,因为InheritedWidget
常被用于数据共享。比如使用频率很高的:Theme/ThemeData
、Text/DefaultTextStyle
、Slider/SliderTheme
、Icon/IconTheme
等内部都是通过InheritedWidget
实现数据共享的。并且Flutter
中部分的状态管理框架,内部的状态共享方法也是基于InheritedWidget
去实现的。
InheritedWidget继承自ProxyWidget,本身并不具备绘制的能力,但共享这个Widget等与共享Widget内保存的数据,获取Widget就可以获取到其内部保存的数据,如下图:
image-20210317145532940.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);
});
}
网友评论