0%

FlutterBoost集成后WillPopScope失效问题

问题出现场景一: 在集成FlutterBoost后,打开flutter页面WillPopScope包裹页面,其对返回手势拦截的功能没有生效,手势被原生容器捕捉,由原生路由处理

问题出现场景二: 在集成FlutterBoost后,打开flutter页面WillPopScope包裹页面,部分页面生效,但onWillPop返回true和flase效果一致。

定位问题

flutter源码flutter_boost/lib/src/boost_lifecycle_binding.dartBoostLifecycleBinding类的containerDidShow方法会根据当前容器flutter页面数量,选择是否禁用原生页面手势:

1
2
3
4
5
6
7
8
9
10
11
12
void containerDidShow(BoostContainer container) {
///When this container show,we check the nums of page in this container,
///And change the pop gesture in this container
if (container.pages.length >= 2) {
BoostChannel.instance
.disablePopGesture(containerId: container.pageInfo.uniqueId!);
} else {
BoostChannel.instance
.enablePopGesture(containerId: container.pageInfo.uniqueId!);
}
...忽略部分
}

上面的代码中enablePopGesture是像原生容器发送消息开启原生容器左滑手势;disablePopGesture 是像原生容器发送消息禁止原生容器左滑手势;

这个event我们在业务代码中也可以调用,会覆盖这部分代码,使用时候需慎重,因为同一个原生容器中,也可能包含多个flutter页面。强制开启手势,左滑返回会将多个flutter页面一同返回。enablePopGesture 事件,也是通过uniqueId作为key,容器和业务代码建成的通讯channel,该channel目前只实现了对enablePopGesture 的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// enable iOS native pop gesture for container matching [containerId]
void enablePopGesture({required String containerId}) {
assert(containerId.isNotEmpty);
BoostChannel.instance.sendEventToNative(containerId, {
'event': 'enablePopGesture',
"args": {'enable': true}
});
}

/// disable iOS native pop gesture for container matching [containerId]
void disablePopGesture({required String containerId}) {
assert(containerId.isNotEmpty);
BoostChannel.instance.sendEventToNative(containerId, {
'event': 'enablePopGesture',
"args": {'enable': false}
});
}

问题分析

  1. 在容器的原生手势开启的时候,WillPopScope是拿不到手势相应的,侧滑会由原生路由处理,这个时候添不添加WillPopScope都没有效果
  2. 在容器的原生手势禁止的时候,页面根据是否包含WillPopScope来判断是否处理flutter侧滑手势,如果有侧滑手势,侧滑路由修的是flutter路由部分,原生容器不受影响。这里注意了,原生容器侧滑手势被禁止,即当当前容器中flutter为路由栈退出后仅剩一个,侧滑手势无人响应,需要手动将原生侧滑手势打开。

containerDidShow 判断page数量是根据容器计算的,也就是我们在跳转flutter页面的时候,带上withContainer = true后,每个flutter页面均会独享一个原生容器。

BoostNavigator.instance.push

1
2
3
4
5
6
Future<T> push<T extends Object?>(
String name, {
Map<String, dynamic>? arguments,
bool withContainer = false,
bool opaque = true,
})

遗留问题

  1. 在容器的原生手势禁止的时候,页面不会根据WillPopScope的onWillPop返回值来判断是否响应侧滑手势,而是均不可侧滑。

通过源码接入三方库正常使用,framework产物接入部分功能失效

踩坑场景

flutter在使用permission_handler库时,flutter通过源码接入宿主工程时,iOS申请权限系统弹窗正常弹出。通过打包framework方式接入宿主工程时,iOS申请系统弹窗不弹,直接返回被永久拒绝。

问题定位路线

  1. 下载permission_handler库源码,提交到私有仓库
  2. 增加日志,定位问题原因为permission_handler使用了宿主工程GCC_PREPROCESSOR_DEFINITIONS配置的一个宏定义。
  3. GCC_PREPROCESSOR_DEFINITIONS是用于设置预处理器宏的。这些宏在编译时会被替换。然而,这些宏只能在编译时期的源代码中使用,不能在运行时期的代码中使用。因此,如果你在framework中尝试读取这些宏,是无法读取到的。
  4. permission_handler的iOS部分源码,通过宏是判断业务代码是否开启了某一权限白名单,获取到宏且宏定义为1,即调用申请权限流程。否则直接返回权限被永久拒绝。

解决方案

通过设置permission_handler的iOS源码中默认宏配置,开启对应白名单。打包提交私有库,页面代码修改 permission_handler路径指向为修改后的私有库地址。

Flutter 各种列表曝光方案

目前的曝光有两大场景:播放器可视播放和元素曝光
其中元素曝光还需要细分,有主轴方向滑动曝光,还有混合纵轴方向滑动曝光

场景一:滑动中可视元素内选取元素播放或者停止播放

场景特点

需要在滑动中获取当前可视元素和移除可视范围的元素

  1. 先判断移除移除可视的元素是否存在播放,如果有,停止播放。
  2. 在上一步执行完毕之后,判断新的可视元素中是否含符合播放标准
  3. 开启下一轮播放
  4. 播放结束前没有被划走,继续监测其他符合播放的元素播放

曝光特点

  1. 实时监听滑动中视口内元素变化
  2. 在监听回调中需要同时拿到当前所有可视元素,进行规则匹配

技术方案

flutter_scrollview_observer

API
1
2
3
4
5
/// The callback of getting observed result map.
final Function(Map<BuildContext, M>)? onObserveAll;

/// The callback of getting observed result for first sliver.
final Function(M)? onObserve;

sliverListContexts如果不为null,列表会通过element.visitChildren方法获取的sliverlist列表。

onObserveAll :是返回所有sliverlist回调

注意:onObserveAll返回所有sliverlist,并不能一一绑定。需要通过函数返回的Map实例的key进行查找。

onObserve :是返回第一个sliverlist回调

场景二:可视曝光

元素进入屏幕视口内回调,一般用于曝光打点

场景特点

  1. 用于列表元素曝光打点

曝光特点

  1. 每次曝光处理,元素独自处理,和列表其他元素曝光没有耦合。

技术方案

flutter_exposure

  • ScrollDetailProvider嵌套滑动列表,监听滑动通知并根据需求转化通知到子组件
  • Exposure嵌套需要统计的单个元素,Exposure接受ScrollDetailProvider转化的通知,根据自身的偏移量判断是否在视口内。
  • ExposureController实例可以绑定多个Exposure,通过调用reCheckExposeState方法手动获取一遍当前视口曝光情况

场景三:可视曝光——特殊组合

我们在监测主轴曝光的同时,对特殊元素需要监测纵轴方向曝光。

场景特点

  • 曝光列表中特殊元素曝光
  • 适用于任何renderObject

曝光特点

  • 每次曝光处理,元素独自处理,和列表其他元素曝光没有耦合。

技术方案

visibility_detector
个人认为效率不高,而且回调次数较多。

  • 需要监听的组件通过该三方组件直接嵌套

  • 监听onVisibilityChanged回调,通过可视比例判断是否满足曝光

  • 在元素paint的方法中调用元素(PaintingContext)的addCompositionCallback方法,

  • 在上述方法中,调用计算任务去判断可视比例

  • 计算任务,通过获取父级列表(到顶端),通过recttransform计算当前元素的bounds,组合成VisibilityInfo

    1
    2
    3
    4
    5
    final List<ContainerLayer> ancestors = <ContainerLayer>[ContainerLayer()];
    while (ancestor != null && ancestor.parent != null) {
    ancestors.add(ancestor);
    ancestor = ancestor.parent;
    }
  • VisibilityInfo中有visibleBounds——实际可视的区域,visibleFraction——显示比例

Kalman Filter

Kalman滤波被称为线性二次型估计。其也可以认为是一个最优化自回归数据处理算法(optimal recursive data processing algorithm)。前提是这个数据变化是线性变化的。

对于其原理我们可以参考Kalman Filter。文章写的很专业,我就不废话。

应用场景

当我们要观察一个仪器测量值,比如力度、重量等测量,在采集过程中,我们会不断拿到新数据,这些数据是采集器从目标中收集出来,是属于线性变化的。但采集器的精准度和目标物体的都含有不确定行。我们需要对这些数据进行过滤,生成平滑的数据。目标如下图:

蓝线是原始数值,绿线是我们想要的结果。

Kalman Filter dart(Flutter)代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class KalmanFilter {
final double q; // 过程噪声协方差
final double r; // 测量噪声协方差
double x; // 当前状态
double p; // 当前状态的协方差
late double k; // 卡尔曼增益

KalmanFilter({this.q = 0.1, this.r = 0.1, this.x = 0, this.p = 1});

double update(double measurement) {
// 预测步骤
final x_pred = x;
final p_pred = p + q;

// 更新步骤
k = p_pred / (p_pred + r);
x = x_pred + k * (measurement - x_pred);
p = (1 - k) * p_pred;

return x; // 返回滤波后的值
}
}

过程噪声协方差

它用来描述状态量在一个时刻到下一个时刻之间的变化幅度。一般来说,过程噪声方差是由系统的动态特性和采样间隔决定的。

如果系统的动态特性比较快,即状态量在一个时刻到下一个时刻之间的变化幅度比较大,那么过程噪声方差就应该设置得比较大,以便更好地反映状态量的变化。反之,如果系统的动态特性比较慢,即状态量在一个时刻到下一个时刻之间的变化幅度比较小,那么过程噪声方差就应该设置得比较小,以便更好地适应状态量的变化。

另外,采样间隔也会影响过程噪声方差的定义。如果采样间隔比较大,即状态量的变化相对较慢,那么过程噪声方差应该设置得比较小,以充分利用状态量的变化信息。反之,如果采样间隔比较小,即状态量的变化相对较快,那么过程噪声方差应该设置得比较大,以避免过度拟合状态量的变化。

因此,过程噪声方差的定义需要考虑系统的动态特性和采样间隔,需要进行实验和分析来确定最佳值。一般来说,过程噪声方差可以通过试验和经验得到,也可以使用自适应卡尔曼滤波器来自动调整。

观测噪声协方差

观测噪声协方差用来描述测量噪声对状态估计的影响。通常情况下,观测噪声协方差是由测量设备的精度和信噪比等因素决定的。

如果测量设备的精度越高,即测量误差越小,那么观测噪声协方差就应该设置得比较小,以更准确地反映状态量的真实值。反之,如果测量设备的精度较低,即测量误差较大,那么观测噪声协方差就应该设置得比较大,以充分考虑测量误差的影响。

另外,信噪比也会影响观测噪声协方差的定义。如果信噪比越高,即信号强度比噪声强度高,那么观测噪声协方差就应该设置得比较小,以更好地反映状态量的真实值。反之,如果信噪比较低,即信号强度比噪声强度低,那么观测噪声协方差就应该设置得比较大,以充分考虑噪声的影响。

因此,观测噪声协方差的定义需要考虑测量设备的精度和信噪比等因素,并且需要根据具体的应用场景进行实验和分析来确定最佳值。一般来说,可以通过实际测量数据的分析和处理来估计观测噪声协方差的值。

基础使用

1
2
3
4
5
6
7
8
9
// 初始化
final kalmanFilter =
KalmanFilter(q: 0.01, r: 0.1, x: originDatas.first.toDouble(), p: 1);
// 接收过滤结果
List<double> fitterRes = [];
for (final measurement in originDatas) {
final filteredMeasurement = kalmanFilter.update(measurement.toDouble());
fitterRes.add(filteredMeasurement);
}

上面这个代码片段只是简单过滤一组数据。在真实使用中当前状态协方差p和卡尔曼增益k会在每次检测流程中根据历史调整,而不是每次update的时候新建。所以是最好是通过一个单例的管理类去管理每次检测流程。

事件分发机制

  1. 命中测试
  2. 事件分发
  3. 事件清理

命中测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// A RenderProxyBox subclass that allows you to customize the
/// hit-testing behavior.
abstract class RenderProxyBoxWithHitTestBehavior extends RenderProxyBox{

@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
bool hitTarget = false;
if (size.contains(position)) {
hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position);
if (hitTarget || behavior == HitTestBehavior.translucent) {
result.add(BoxHitTestEntry(this, position));
}
}
return hitTarget;
}

@override
bool hitTestSelf(Offset position) => behavior == HitTestBehavior.opaque;

...
}
  • 手指按下触发PointerDownEvent事件,按照深度优先开始进行命中测试
  • 如果子组件命中返回为ture,则先被加入命中结果数组,其父组件同样也会返回true,并加入命中结果数组。这一点在接口RenderProxyBoxWithHitTestBehaviorhitTest方法中体现——hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position))。
  • 这里要注意如果是多个子组件,是如何去判断命中的。这里我们看一下RenderBoxContainerDefaultsMixins的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool defaultHitTestChildren(BoxHitTestResult result, { required Offset position }) {
ChildType? child = lastChild;
while (child != null) {
// The x, y parameters have the top left of the node's box as the origin.
final ParentDataType childParentData = child.parentData! as ParentDataType;
final bool isHit = result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.offset);
return child!.hitTest(result, position: transformed);
},
);
if (isHit) {
return true;
}
child = childParentData.previousSibling;
}
return false;
}

多个子组件,遍历子组件(从后往前遍历),有命中的就终止遍历,返回结果。

为什么从后往前(previousSibling),而不是从前往后(nextSibling)?

自问自答:我个人理解是命中区域会有重叠,如Stack。当Stack有重叠的时候,最上层显示children中靠后的,当然事件也优先让他们去命中测试。

命中测试当前作用手机了命中结果数组HitTestResult

事件分发

GestureBinding类为单例类,集成自BindingBase,并从BindingBase中获取platformDispatcher,并对platformDispatcher的触摸事件处理进行方法进行赋值。
事件分发调用是由platformDispatcher单例去管理的。

1
ui.PlatformDispatcher get platformDispatcher => ui.PlatformDispatcher.instance

GestureBinding管理Queue,调用_handlePointerEventImmediately并在合适时机遍历分发event到目标上,包括hitTest命中测试方法,

dispatchEvent方法先判断事件类型,如果HitTestResult有结果,遍历调用HitTestTargethandleEvent方法,组件只需要重新handleEvent方法去处理事件就好。对于我们日常用的ListenerGestureDetector(Listener的封装)均在ListenercreateRenderObject方法返回的RenderPointerListener中实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent) {
return onPointerDown?.call(event);
}
if (event is PointerMoveEvent) {
return onPointerMove?.call(event);
}
if (event is PointerUpEvent) {
return onPointerUp?.call(event);
}
if (event is PointerHoverEvent) {
return onPointerHover?.call(event);
}
if (event is PointerCancelEvent) {
return onPointerCancel?.call(event);
}
if (event is PointerPanZoomStartEvent) {
return onPointerPanZoomStart?.call(event);
}
if (event is PointerPanZoomUpdateEvent) {
return onPointerPanZoomUpdate?.call(event);
}
if (event is PointerPanZoomEndEvent) {
return onPointerPanZoomEnd?.call(event);
}
if (event is PointerSignalEvent) {
return onPointerSignal?.call(event);
}
}

handleEvent根据具体的事件类型,调用传入的回调方法。

至此事件完整的分发出去了。

事件清理

事件清理也是在_handlePointerEventImmediately方法中进行,当收到upcancel等事件后,进行相应的分发,并从hitTestResult中移除

记录个人学习过程,不对的地方欢迎大佬指正

自定义flex组件,获取子组件布局信息

我们都在用ColumnRow,他们都是Flex子类,将children中组件纵向或者横向进行摆放。

现在有一个需求是摆放完成后,需要知道children中各个widget所占的宽或者高,从而知道他们具体的位置锚点,然后可以根据这些锚点进行对children添加精准动画

目标效果

我们先来了解一下需要用到的基础知识点Flex

Flex

根据主轴方向显示一个一维widget数组。

其中ColumnRow就是Flex子类,他们直接把主轴方向写死简单的进行一次封装。

Flex集成自MultiChildRenderObjectWidget,实现两个主要方法createRenderObjectcreateRenderObject,这两个方法分别被Element在挂载(mount)和更新(update)的时候调用。

所以Flex只是MultiChildRenderObjectWidget的一个子类,具体布局还是父类去实现的,Flex通过重写createRenderObject指定返回元素RenderFlex,具体元素的加载是在RenderFlex中执行的,我们想第一时间拿到布局信息,也是在RenderFlexperformLayout中获取

RenderObjectElement 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
abstract class RenderObjectElement extends Element {
/// Creates an element that uses the given widget as its configuration.
RenderObjectElement(RenderObjectWidget super.widget);
···

RenderObject? _renderObject;

@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
···
_renderObject = (widget as RenderObjectWidget).createRenderObject(this);
···
super.performRebuild(); // clears the "dirty" flag
}

@override
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
···
_performRebuild(); // calls widget.updateRenderObject()
}

@pragma('vm:prefer-inline')
void _performRebuild() {
···
(widget as RenderObjectWidget).updateRenderObject(this, renderObject);

···
super.performRebuild(); // clears the "dirty" flag
}
}

  • mount: RenderObject 对象的创建,以及与渲染树的插入工作,插入到渲染树后的 Element 就可以显示到屏幕中了。
  • update: 如果 Widget 的配置数据发生了改变,那么持有该 Widget 的 Element 节点也会被标记为 dirty。在下一个周期的绘制时,Flutter 就会触发 Element 树的更新,并使用最新的 Widget 数据更新自身以及关联的 RenderObject 对象,接下来便会进入 Layout 和 Paint 的流程。而真正的绘制和布局过程,则完全交由 RenderObject 完成。

自定义Flex组件

这里我们先自定义flex组件,想要拿到并更新我们的位置信息,我们就得重新两个重要方法:createRenderObjectcreateRenderObject

定义一个LayoutCallback回调类型,在拿到我们想要的布局后通过这个回调去处理,并在需要更新的时候去调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

typedef LayoutCallback = void Function(
List<double> xOffsets, TextDirection textDirection, double width);

class TabLabelBar extends Flex {
TabLabelBar({
Key? key,
List<Widget> children = const <Widget>[],
required this.onPerformLayout,
}) : super(
key: key,
children: children,
direction: Axis.horizontal, // 根据自己需求
);

final LayoutCallback onPerformLayout;

@override
RenderFlex createRenderObject(BuildContext context) {
return TabLabelBarRenderer(
direction: direction,
mainAxisAlignment: mainAxisAlignment,
mainAxisSize: mainAxisSize,
crossAxisAlignment: crossAxisAlignment,
textDirection: getEffectiveTextDirection(context)!,
verticalDirection: verticalDirection,
onPerformLayout: onPerformLayout,
);
}

@override
void updateRenderObject(
BuildContext context, _TabLabelBarRenderer renderObject) {
super.updateRenderObject(context, renderObject);
renderObject.onPerformLayout = onPerformLayout;
}
}
  • createRenderObject Element在挂载(mount)到树上通过调用该方法,去创建widget对应的renderObject。
  • updateRenderObject 在Widget配置数据更新后,修改对应的 Render Object。该方法在首次 build 以及需要更新 Widget 时都会调用

去实现createRenderObject方法

Flex的createRenderObject方法返回的RenderFlex。接下来要做的就是去实现自己的RenderFlex,肯定是去继承RenderFlex,重写相关方法,在重写中拿到我们想要的数据并回调。

  1. 将回调方法传入我们定义RenderFlex子类
  2. 在子类layout完毕的时候拿到布局信息,整合并回调
    1. RenderFlex继承RenderBox,RenderBox混入ContainerRenderObjectMixin 、 RenderBoxContainerDefaultsMixin 和 ContainerBoxParentData,通过 ContainerBoxParentData ,我们可以将 RenderBox 需要的 BoxParentData 和上面的 ContainerParentDataMixin 组合起来,事实上我们得到的 children 双链表就是以 ParentData 的形式呈现出来的。
    2. 要拿到所有同级widget布局,就要去先去拿到父级的renderObject
    3. 要拿到父级renderObject,我们可以通过其中一个子级的parentData
    4. ContainerRenderObjectMixin中的firstChild、lastChild、childCount。RenderFlex中可以直接能达到第一个元素firstChild。
    5. firstChild拿到parentData,然后就准确拿到了同级的RenderBox
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

class TabLabelBarRenderer extends RenderFlex {
TabLabelBarRenderer({
List<RenderBox>? children,
required Axis direction,
required MainAxisSize mainAxisSize,
required MainAxisAlignment mainAxisAlignment,
required CrossAxisAlignment crossAxisAlignment,
required TextDirection textDirection,
required VerticalDirection verticalDirection,
required this.onPerformLayout,
}) : super(
children: children,
direction: direction,
mainAxisSize: mainAxisSize,
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
textDirection: textDirection,
verticalDirection: verticalDirection,
);

LayoutCallback onPerformLayout;

@override
void performLayout() {
super.performLayout();
// xOffsets will contain childCount+1 values, giving the offsets of the
// leading edge of the first tab as the first value, of the leading edge of
// the each subsequent tab as each subsequent value, and of the trailing
// edge of the last tab as the last value.
RenderBox? child = firstChild;
final List<double> xOffsets = <double>[];
while (child != null) {
final FlexParentData childParentData =
child.parentData! as FlexParentData;
xOffsets.add(childParentData.offset.dx);
assert(child.parentData == childParentData);
//nextSibling: The next sibling in the parent's child list.
child = childParentData.nextSibling;
}
assert(textDirection != null);
switch (textDirection!) {
case TextDirection.rtl:
xOffsets.insert(0, size.width);
break;
case TextDirection.ltr:
xOffsets.add(size.width);
break;
}
onPerformLayout(xOffsets, textDirection!, size.width);
}
}

使用

到此,我们已经封装完毕。

1
2
3
4
TabLabelBar(
onPerformLayout: _saveOffsets,
children: [...widgets],
),

Notification

NotificationListener也是冒泡传递,和触摸Listener不同的是:NotificationListener可以通过onNotification阻止继续冒泡,而Listener不行。

通知使用流程

通知类,每个节点都可以发送通知Notification,每个节点也都可以接收通知。通知通过冒泡逐级往父类查找对于泛型的NotificationListener监听。

下面我们先制定一个通知类继承Notification, 走一遍通知流程:

自定义通知类

1
2
3
4
5
6
7
import 'package:flutter/material.dart';

class RZNotification extends Notification {
final int count;
RZNotification({required this.count}) : super();
}

发送通知

通过通知实例,调用dispatch方法,dispatch方法默认在抽象类Notification实现

1
2
RZNotification notification = RZNotification(count: 5);
notification.dispatch(context);

监听通知

1
2
3
4
5
6
7
NotificationListener<RZNotification>(
onNotification: (RZNotification rzNotification) {
print('===rzNotification==00=${rzNotification.count}');
return true;
},
child:

NotificationListener

监听类只有两个成员child和onNotification,一个私有方法(_dispatch),一个build方法,build方法直接返回传入child。

_dispatch

通知类通过element树查找所有通知监听类NotificationListener ,调用监听类的_dispatch,并传入通知类实例。_dispatch 方法校验是否属于自己监听的泛型类,如果是并且onNotification方法返回为true。

接收Notification对应泛型子类的通知,对于泛型子类通过dispatch(BuildContent? target)方法——通过element树层层往上级查找,找到对应泛型的Listener,即调用onNotification回调。

再看Notification

dispatch(BuildContent? target)

Notification类为抽象类,子类直接调用dispatch方法,通过传入BuildContentvisitAncestorElements查找,并传入查找逻辑(visitor)

visitAncestorElements是BuildContent实例方法:

1
2
3
4
5
6
7
@override
void visitAncestorElements(bool Function(Element element) visitor) {
assert(_debugCheckStateIsActiveForAncestorLookup());
Element? ancestor = _parent;
while (ancestor != null && visitor(ancestor))
ancestor = ancestor._parent;
}

个人理解是传入一个访问逻辑,visitAncestorElements逐级往上查找父类——do循环,如果父类不为空,将父类传入访问逻辑中,如果访问逻辑返回true就继续查找改父类的父类,如果返回false,就停止查找。

visitAncestor

访问逻辑visitAncestor,并不知道是那个子类调用的dispatch方法,所以先查找widget is NotificationListener<Notification>,是的话就调用NotificationListener的_dispatch方法,并传入调用者,NotificationListener都知道自己监听的泛型类是什么,如果是调用者是自己监听的类,即调用onNotification回调。

1
2
3
4
5
6
7
8
9
10
11
12
@protected
@mustCallSuper
bool visitAncestor(Element element) {
if (element is StatelessElement) {
final StatelessWidget widget = element.widget;
if (widget is NotificationListener<Notification>) {
if (widget._dispatch(this, element)) // that function checks the type dynamically
return false;
}
}
return true;
}

总结

通过NotificationListener和Notification配合,我们清晰知道了通知发送和接收的逻辑。通知这种方式的通信也有它的局限性,监听者只能监听其子widget发送的通知。所以我们也需要根据应用场景实际情况选择性去使用。

并且熟悉了Context和Element关系:

  • Context遍历Element树的方法
  • 可以通过Element.widget得到element节点对应的widget

ListView

inheritance 继承关系

Object > DiagnosticableTree > Widget > StatelessWidget > ScrollView > BoxScrollView > ListView

BoxScrollView

BoxScrollView 只有一个成员padding,根据滚动方向来添加padding,将另一方向的padding置为0。

BoxScrollView 需要传入的参数有很多,几乎多有传入参数均传给了其父类ScrollView

ScrollView

提供滚动(Scrollable)组件、viewport。Scrollable在滚动中回调函数viewportBuilder,提供给viewport偏移量,返回显示视图。

统一处理滚动过程中键盘处理——键盘失去焦点,通过FocusScope.of(context)获取当前焦点状态,调用unfocus方法房键盘消失。

ListView总结

在查看集成链路源码中,通过抽象方法划分功能模块。

  • ScrollView: 负责滚动相关功能

    • 集成Scrollable,实现滚动能力。通过集成Scrollable的返回的偏移量,管理自己的viewport。
    • 视口创建,Scrollable的回调方法viewportBuilder也很好的解耦了滚动功能和视口创建,视图功能交给使用者ScrollView, 由ScrollView的buildViewport方法实现。
    • 视图内容交给子类实现,在buildViewport方法中需要传入具体sliver,而sliver方法是ScrollView的抽象方法,交给子类实现(如BoxScrollView)
  • BoxScrollView: 负责处理滚动边距问题

    • 实现父类ScrollView的buildSlivers方法,处理边距问题
    • 具体sliver内容通过抽象方法buildChildLayout,交由子类实现
  • ListView: 实现buildChildLayout方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @override
    Widget buildChildLayout(BuildContext context) {
    if (itemExtent != null) {
    return SliverFixedExtentList(
    delegate: childrenDelegate,
    itemExtent: itemExtent!,
    );
    } else if (prototypeItem != null) {
    return SliverPrototypeExtentList(
    delegate: childrenDelegate,
    prototypeItem: prototypeItem!,
    );
    }
    return SliverList(delegate: childrenDelegate);
    }

GirdView

inheritance

Object > DiagnosticableTree > Widget > StatelessWidget > ScrollView > BoxScrollView > GridView

GridView和ListView都是BoxScrollView子类。边距处理、滚动功能、视口创建均由父类实现。他们均需要实现buildChildLayout方法,去管理具体silver内容

GirdView

有两个成员girdDelegate和childrenDelegate,这两个类均默认在GirdView构造函数中创建。

  • girdDelegate成员是SliverGridDelegate类型,作用是管理sliver布局约束,SliverGridDelegate是抽象类,它有两个子类,一般我们使用也是使用这两个子类:
    • SliverGridDelegateWithFixedCrossAxisCount,通过交叉轴sliver个数
    • SliverGridDelegateWithMaxCrossAxisExtent, 通过sliver在交叉轴最大尺寸
  • childrenDelegate成员是SliverChildDelegate类型,girdView布局管理,给SliverGrid提供最大滚动距离。SliverChildDelegate也是抽象类,也是两个子类,一个用于有builder回调的,一个用于列表:
    • SliverChildBuilderDelegate
    • SliverChildListDelegate

实现buildChildLayout方法

1
2
3
4
5
6
7
@override
Widget buildChildLayout(BuildContext context) {
return SliverGrid(
delegate: childrenDelegate,
gridDelegate: gridDelegate,
);
}

GirdView总结

GirdView主要是通过两个成员变量来管理sliver布局和尺寸——childrenDelegate和gridDelegate,这两个成员的类型均是抽象类,也就是说我们完全可以通过集成相应抽象类,去具体实现我们想要的布局,让GirdView灵活性变得大很多。

flutter布局的核心

Constraints go down, Sizes go up, Parent sets position

  • 父节点向子节点传约束
  • 子节点向父节点上传大小
  • 最后由父节点决定位置

不是按照直接约束显示

问题代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Scaffold(
body: Center(
child: ConstrainedBox(
constraints: BoxConstraints.tight(const Size(300, 300)),
child: ColoredBox(
color: Colors.yellow,
child: ConstrainedBox(
constraints: BoxConstraints.tight(const Size(100, 200)),
child: const ColoredBox(
color: Colors.red,
child: SizedBox(
width: 100,
height: 100,
child: ColoredBox(color: Colors.teal),
),
),
),
),
),
),
);

显示效果

最终只显示了蓝绿色,而且没有按照我们对蓝绿色组件直接的约束大小显示。

错误分析

  • 这段代码,我们逐步移出蓝绿色、黄色代码,发现黄色和红色均显示为300*300
  • 我们想显示的蓝绿色、红色均没有按照我们直接的约束显示,都被黄色父节点约束覆盖了。
  • 而黄色可以理解为按照起父级约束显示。

我们发现黄色的父级约束盒子的父级是Center, 这个就是重点

  1. Center的继承关系是:SingleChildRenderObjectWidget>Align>Center
  2. ConstrainedBox的继承关系是:SingleChildRenderObjectWidget>ConstrainedBox
  3. SizedBox的继承关系是:SingleChildRenderObjectWidget>SizedBox

这里先插入一个知识,我们在屏幕上看到的UI都是通过RenderObjectWidget实现的。而RenderObjectWidget中有个creatRenderObject方法生成RenderObject对象,RenderObject实际负责layout()和paint()。

其中Align的creatRenderObject方法返回的是RenderPositionedBox;

SizedBox ConstrainedBox的creatRenderObject方法返回的是RenderConstrainedBox;

RenderPositionedBox 和 RenderConstrainedBox

RenderPositionedBoxRenderConstrainedBox 最终集成的都是RenderBox

我们先来对比一下用于计算布局的performLayout方法

RenderPositionedBox

这里在子节点渲染时,修改了布局约束,改为松约束。
其中constraints.loosen()是把min约束给删除,可以理解为置零

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;

if (child != null) {
// 这里在子节点渲染时,修改了布局约束,改为松约束。
// 其中constraints.loosen()是把min约束给删除,可以理解为置零
child!.layout(constraints.loosen(), parentUsesSize: true);
// 根据子节点大小计算自己的大小
// 默认是没设置Factor 那么自身size就是最大值double.infinity
size = constraints.constrain(Size(
shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity,
));
// 根据自身size,子节点size,来绘制子节点位置
alignChild();
} else {
size = constraints.constrain(Size(
shrinkWrapWidth ? 0.0 : double.infinity,
shrinkWrapHeight ? 0.0 : double.infinity,
));
}
}

RenderConstrainedBox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
if (child != null) {
// 直接把自己的约束传递给子节点,并获取子节点size
child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
// 将子节点的size赋值给自己
size = child!.size;
// 自己和子节点的size相同,所以不需要进行align子节点
} else {
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}

结论

我们可以得到结论,当直接父节点是CenterAlign等约束Widget,他们的creatRenderObject返回的是RenderPositionedBox, 而RenderPositionedBox计算会先计算子节点大小,然后结算自己的大小,最后根据约束子组件偏移,子组件是可以显示其自身约束,前提是在RenderPositionedBox的松约束下。

而ConstrainedBox、SizeBox等最终的creatRenderObject返回的是RenderConstrainedBox,其直接把父节点约束传递给子节点,当父节点的约束是紧约束,即是expand显示,所有creatRenderObject返回的是RenderConstrainedBox的子节点均会继承约束,即自身约束并不生效。