Flutter visibility_detector 原理解析
简介
visibility_detector 是由谷歌官方团队开发的一款拓展组件,主要用于监听子组件可见性的变化。
背景
日常开发场景中,经常遇到一些需求需要在 Widget 可见性发生变化的时候进行操作,比如视频播放组件,需要让其在切换页面后自动停止播放,又比如 Banner 轮播图组件,需要在 Banner 过页时,让其停止轮播以达到优化性能的效果。
那么应该怎么去实现?
首先会想到的是用State的生命周期,initState 初始化,dispose 关闭,但这有一个问题,push一个新页面是不会触发 dispose 的,所以这是行不通的。
那么用 WidgetsBindingObserver 呢?
WidgetsBinding.instance.addObserver(this);
...
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if(state == AppLifecycleState.paused){
...
} else if(state == AppLifecycleState.resumed){
...
}
}
如果亲自尝试会发现这也是行不通的,因为 didChangeAppLifecycleState 只会监听整个app的变化,而不是监听当前widget,在push一个新页面时也不会触发回调。
最后发现实现方案只能用 NavigatorObserver 去监听路由栈的变化,在 didPush 和 didPop 中获取路由信息并手动维护一个全局的 Manager,Widget 在使用时向 Manager 去注册路由变化的通知回调同时配合 WidgetsBindingObserver 就可以了。但这未免也太麻烦了,难道没有更简单的方法吗?
直到最后我发现了 visibility_detector ,使用方法如下,是不是很简单?
@override
Widget build(BuildContext context) {
return VisibilityDetector(
key: Key('my-widget-key'),
onVisibilityChanged: (visibilityInfo) {
var visiblePercentage = visibilityInfo.visibleFraction * 100;
debugPrint(
'Widget ${visibilityInfo.key} is ${visiblePercentage}% visible');
},
child: someOtherWidget,
);
}
原理解析
VisibilityDetector 使用的不是如上所说的 NavigatorObserver 方法,而是另辟蹊径,采用了更巧妙的方法。
那是如何监听显示状态发生变化的呢?看看源码便知,VisibilityDetector 的源码很简单,基本实现功能只用了三个类。
class VisibilityDetector extends SingleChildRenderObjectWidget {
const VisibilityDetector({
@required Key key,
@required Widget child,
@required this.onVisibilityChanged,
}) : assert(key != null),
assert(child != null),
super(key: key, child: child);
final VisibilityChangedCallback onVisibilityChanged;
@override
RenderVisibilityDetector createRenderObject(BuildContext context) {
return RenderVisibilityDetector(
key: key,
onVisibilityChanged: onVisibilityChanged,
);
}
@override
void updateRenderObject(
BuildContext context, RenderVisibilityDetector renderObject) {
assert(renderObject.key == key);
renderObject.onVisibilityChanged = onVisibilityChanged;
}
}
VisibilityDetector 继承自 SingleChildRenderObjectWidget 类,而 SingleChildRenderObjectWidget 想必大家都见过,很多组件如 Align、Padding 等都继承自 SingleChildRenderObjectWidget,用于生成需要渲染的RenderObject。
接下来看 RenderVisibilityDetector 类。
class RenderVisibilityDetector extends RenderProxyBox {
RenderVisibilityDetector({
RenderBox child,
@required this.key,
@required VisibilityChangedCallback onVisibilityChanged,
}) : assert(key != null),
_onVisibilityChanged = onVisibilityChanged,
super(child);
省略一些代码...
@override
void paint(PaintingContext context, Offset offset) {
if (onVisibilityChanged == null) {
// No need to create a [VisibilityDetectorLayer]. However, in case one
// already exists, remove all cached data for it so that we won't fire
// visibility callbacks when the layer is removed.
VisibilityDetectorLayer.forget(key);
super.paint(context, offset);
return;
}
final layer = VisibilityDetectorLayer(
key: key,
widgetSize: semanticBounds.size,
paintOffset: offset,
onVisibilityChanged: onVisibilityChanged);
context.pushLayer(layer, super.paint, offset);
}
}
在 RenderVisibilityDetector 中,在绘制方法 paint() 里 push 了一个 Layer,这个Layer就是 VisibilityDetector 用于监听可见状态的关键。
那么 Layer 是什么?
Layer 表示的是图层的意思。通常一棵 RenderObject 树经过绘制之后,就会生成一个 Layer 对象,但并不是所有 RenderObject 都会绘制到一个 Layer 中,某些情况下,例如不同路由页面,就会绘制到不同的 Layer 图层中。这些 Layer 对象组成的结构就是 Layer 树。
再看回 VisibilityDetectorLayer
class VisibilityDetectorLayer extends ContainerLayer {
...省略一些代码
static Timer _timer;
@override
void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
_scheduleUpdate();
super.addToScene(builder, layerOffset);
}
@override
void attach(Object owner) {
super.attach(owner);
_scheduleUpdate();
}
@override
void detach() {
super.detach();
_scheduleUpdate();
}
void _scheduleUpdate() {
...省略一些代码
if(_timer == null){
_timer = Timer(updateInterval, _handleTimer);
}
}
void _handleTimer(){
_timer == null;
...延迟500ms后,检测layer宽高
}
}
在 Layer 附加到与移除屏幕时, addToScene(),attach(),detach()这三个方法会收到回调,所以这就是检测的时机。在收到回调后,会启动一个延时500ms的定时器,定时器有两个作用,一是确保子 Widget 已绘制完成,二是因为在屏幕在需要多次绘制时可能会添加多个 Layer,所以需要用定时器进行过滤(注意这里 _timer 是 static 类型)。
定时器执行的任务 _handleTimer() 主要是计算 Layer 的宽高,通过 Layer 宽高大小去间接判断子组件是否可见。(注:这里计算宽高的方法比较复杂,这里只作简要说明,深入理解需看源码)
总结
VisibilityDetector 通过在子Widget 上放置一个 Layer 来间接检测可见状态,这是一个很巧妙的做法。
但是也存在局限性,因为是通过宽高判断,所以是无法识别子 Widget 的透明度变化的。
转载自:https://juejin.cn/post/6985043598051901477