likes
comments
collection
share

Flutter 必知必会系列 —— Navigator 的开始 Overlay

作者站长头像
站长
· 阅读数 53

Overlay 的场景是在屏幕上展示悬浮窗,比如 Flutter 版本的 Toast,任意位置的 PopWindow 等等。其实,Overlay 也与 Flutter 的路由管理有着密不可分的联系,Navigator 就是使用 Overlay 实现了页面叠加的效果。作为 Navigator 系列的开始,我们就先介绍 Overlay

Flutter 必知必会系列 —— Navigator 的开始 Overlay

Overlay 是什么

我们先看文档是怎么介绍的。

A stack of entries that can be managed independently.
Overlay 是一个 维护着entries 的 Stack,并且每一个 entry 是自管理的

Overlays let independent child widgets "float" visual elements on top of
other widgets by inserting them into the overlay's stack. The overlay lets
each of these widgets manage their participation in the overlay using
OverlayEntry objects.
Overlay 让它栈中的 Widget 悬浮在屏幕的其他组件之上。并且 OverlayEntry 可以实现悬浮组件
的自管理,比如插入、移出等等


Although you can create an Overlay directly, it's most common to use the
overlay created by the [Navigator] in a [WidgetsApp] or a [MaterialApp]. The
navigator uses its overlay to manage the visual appearance of its routes.
虽然开发者可以直接创建一个 Overlay 组件,但是大多数的时候开发者可以直接使用 Navigator 
创建的 Overlay。Navigator 就是使用它所创建的 Overlay 来管理路由页面

The [Overlay] widget uses a custom stack implementation, which is very
similar to the [Stack] widget. The main use case of [Overlay] is related to
navigation and being able to insert widgets on top of the pages in an app.
To simply display a stack of widgets, consider using [Stack] instead.
Overlay 和 Stack 非常相似,Overlay 的应用场景主要是:路由管理和悬浮窗口。其他的场景可以
直接使用 Stack 组件

总结下来就是:

Overlay 中维护了一个 OverlayEntry 栈,并且每一个 OverlayEntry 是自管理的

Navigator 已经创建了一个 Overlay 组件,开发者可以直接使用,并且通过 Overlay 实现了页面管理

Overlay 的应用场景有两个:实现悬浮窗口的功能,实现页面叠加

上面的文档介绍了 Overlay 的基本作用,我们再看 Overlay 的代码


///...省略assert
class Overlay extends StatefulWidget {
 
  const Overlay({
    Key? key,
    this.initialEntries = const <OverlayEntry>[],
    this.clipBehavior = Clip.hardEdge,
  }) : super(key: key);

  final List<OverlayEntry> initialEntries;

  final Clip clipBehavior;


  static OverlayState? of(
    BuildContext context, {
    bool rootOverlay = false,
    Widget? debugRequiredFor,
  }) {
    final OverlayState? result = rootOverlay
        ? context.findRootAncestorStateOfType<OverlayState>()
        : context.findAncestorStateOfType<OverlayState>();
    return result;
  }

  @override
  OverlayState createState() => OverlayState();
}

Overlay 是一个 StatefulWidget 组件,我们可以按着 StatefulWidget 的方式去理解它,它的显示逻辑和功能都封装在了 OverlayState 中。

不同的是它提供了一个静态的 of 方法,那么我们就可以使用 of 拿到 OverlayState,执行OverlayState 的方法了。

这里简单介绍一个查找的机制,BuildContext 是抽象的接口,代表了 Widget 树的位置,ElementBuildContext 真正的实现。

/// Returns the nearest ancestor widget of the given type `T`, which must be the
/// type of a concrete [Widget] subclass.

@override
T? findAncestorWidgetOfExactType<T extends Widget>() {
  Element? ancestor = _parent;
  while (ancestor != null && ancestor.widget.runtimeType != T)//第一处
    ancestor = ancestor._parent;
  return ancestor?.widget as T?;
}

我们看到,查找的过程就是一个 while 循环,因为是层序的向上查找,所以算法的角度是 O(N)

OverlayState 提供了插入,重组等方法,后面在详细看

现在我们知道了 Overlay 是什么,下一步尝试使用 Overlay

Overlay 使用

总结下来 Overlay 的使用步骤如下:

第一步:向上查找到 OverlayState

OverlayState state = Overlay.of(context);

第二步:构造 OverlyEntry

OverlayEntry overlayEntry = OverlayEntry(
  builder: (context){
    return _buildWidget();
  }
);

第三步:构造悬浮的 Widget

Widget _buildWidget(){
  return Text("悬浮文本");
}

上面就是使用的具体步骤。额外的注意点有以下几点。

悬浮在指定的位置

上面我们已经知道了 Overlay 就是 Stack 组件,那么我们的悬浮内容,就可以用Positioned 包裹,给它指定的 top、left 即可。

比如 下面的代码,

Positioned(
  top: 100,
  right: 100,
  child: Text("悬浮文本"),
);

上面的文本组件,就可以显示在(100,100)的位置。如下图:

但是有的时候我们想根据某个组件来确定显示的位置,这种情况下我们就需要拿到该组件的位置了。 但是 Widget 是不包含位置信息,位置信息包含在 Render 之中。这里提供两个思路来获取位置信息。

GlobalKey 获取位置信息

Widget _buildWidget() {
  RenderBox renderBox =
      globalKey.currentContext.findRenderObject() as RenderBox;
  Offset position = renderBox.localToGlobal(Offset.zero);

  return Positioned(
    left: position.dx,
    top: position.dy,
    child: Text("悬浮文本"),
  );
}

这个方法的前提是需要将 GlobalKey 作为 Key 属性赋值给想要锚定的组件。这样就可以通过GlobalKey 拿到锚定组件的 RenderObject 了。 有了 RenderObject 我们就可以拿到尺寸、位置等信息。

自定义 RenderObject 获取位置信息

Flutter 有一种冒泡机制的 Notification,我们可以将布局冒泡出去。(其实不仅仅是位置,还可以是尺寸等信息),具体操纵是这样的。

第一步:定义携带位置信息的 Notification

class PositionedNotification extends Notification {
  Offset offset;//位置信息
}

第二步:定义监听位置信息的 Widget

class PositionedNotificationNotifier extends SingleChildRenderObjectWidget {//第一处
  const PositionedNotificationNotifier({
    Key key,
    Widget child,
  }) : super(key: key, child: child);

  @override
  _PositionedCallback createRenderObject(BuildContext context) {
    return _PositionedCallback(onLayoutChangedCallback: (offset) {
      (PositionedNotification()..offset = offset).dispatch(context);//第二处 
    });
  }
}

class _PositionedCallback extends RenderProxyBox {
  _PositionedCallback({
    RenderBox child,
    @required this.onLayoutChangedCallback,
  })  : assert(onLayoutChangedCallback != null),
        super(child);

  final Function(Offset) onLayoutChangedCallback;
  Offset tmp;

  @override
  void performLayout() {
    super.performLayout();

    SchedulerBinding.instance.addPostFrameCallback((time) { //第三处
      tmp = localToGlobal(Offset.zero);
      onLayoutChangedCallback(tmp);//第二处
    });
  }
}

首先看第一处,如果我们想要自定义带有 RenderObject 信息的组件的话,就可以继承自SingleChildRenderObjectWidgetMultiChildRenderObjectWidget,而不用直接继承自 RenderObject。

再看第二处, 我们在 performLayout 中,将位置信息回调给 onLayoutChangedCallback 中,并通过通知的 dispatch 机制,发送出去。

这里注意看第三处。使用了 SchedulerBinding 的帧回调,等首帧绘制完成之后,采取获取尺寸信息。否则的话 还没有绘制完就去拿信息,就会报错,因为这个时候整个布局过程还没完成。

如果不想要这么做的话,我们可以将这个动作放在paint方法中。因为paint的时候布局流程已经结束了。如下:

@override
void paint(PaintingContext context, Offset offset) {
  super.paint(context, offset);
  tmp = localToGlobal(Offset.zero);
  onLayoutChangedCallback(tmp);
}

第三步:使用冒泡

NotificationListener<PositionedNotification>(
  onNotification: (positionedNotification) {
    print(positionedNotification.offset);
    return true;
  },
  child: PositionedNotificationNotifier(child: Text("sss")),
)

这就是两种获取位置信息的方法:一种是通过 GlobalKey,一种是通过自定义 RenderObject,当然自定义 RenderObject 不仅仅冒泡一种方式,还可以通过回调的方式,殊途同归,都是参与到布局过程中,将信息回调出来。

悬浮窗可随意拖动

上面我们通过 Position 实现了显示指定位置的功能,现在再加一点点自由拖动。有两种实现的途径:第一种就是自己添加手势,第二种就是使用系统的组件。我们以第一种手势为例。

示例代码如下:

class __XXXState extends State<XXXWidget> {
  double left;
  double top;

  @override
  void initState() {
    super.initState();
    left = widget.initX ?? 0;
    top = widget.initY ?? 0;
  }

  @override
  Widget build(BuildContext context) {
    return Positioned(
      top: top,
      left: left,
      child: GestureDetector(//第一处
          onPanUpdate: (detail) {
            setState(() {//第二处
              top += detail.delta.dy;
              left += detail.delta.dx;
            });
          },
          child: widget.floatWidget),
    );
  }
}

第一处:在原有组件的基础上,增加了手势识别组件 GestureDetecto

第二处:跟随手势,实时 setState 新的坐标。delta 就是 xy 的偏移量。

这就是基本的实现过程,当然我们也可以在里面加入类似边缘吸附的效果。

系统组件中实现拖拽的有:DraggableDragTarget,这一对的具体实现过程可以参考 👉 Flutter 拖拽控件Draggable看这一篇就够了

现在我们基本就可以灵活使用 Overlay 实现任意类型的悬浮窗了。下面我们看为啥它可以做到这一点。

Overly 是怎么做到的

Overly 的显示和管理和 OverlayEntry 密不可分,我们先从 OverlayEntry 开始。

OverlayEntry 是核心

A place in an [Overlay] that can contain a widget.
Overlay 中的一个小单元,可以包含一个 Widget

Overlay entries are inserted into an [Overlay] using the
[OverlayState.insert] or [OverlayState.insertAll] functions. 
To find the closest enclosing overlay for a given
[BuildContext], use the [Overlay.of] function.
我们可以使用 [OverlayState.insert] 或者 [OverlayState.insertAll]
方法将 OverlayEntry 插入到 Overlay 中。并且可以使用Overlay.of向上找
到最近的OverlayState

An overlay entry can be in at most one overlay at a time. To
remove an entry from its overlay, call the [remove] function
on the overlay entry.
OverlayEntry 在移出之前只能在 Overlay 中插入一次。可以调用 remove 方
法移除这个 entry

Because an [Overlay] uses a [Stack] layout, overlay entries
can use [Positioned] and [AnimatedPositioned] to position
themselves within the overlay.
由于 Overlay 内部使用的是 Stack 组件,所以 overlayEntry 包含的组件可
以是 [Positioned][AnimatedPositioned]

总结一下:

OverlayEntry 中承载这 Overlay 要显示的 Widget,并且是自管理的,可以插入和删除

Overlay 是 Stack 组件的包裹,所以 OverlayEntry 的 Widget 可以使用 Position 来显示在指定的位置

opaquemaintainState 属性影响着三棵树的形成

下面我们看 OverlayEntry 是怎么包裹我们给它的组件的。

class OverlayEntry extends ChangeNotifier {
 
  OverlayEntry({
    required this.builder, //第一处
    bool opaque = false,
    bool maintainState = false,
  }) : _opaque = opaque,
       _maintainState = maintainState;

  final WidgetBuilder builder;
  //..省略代码
}

OverlayEntry 的作用是要承载我们想要显示的 Widget。但是我们看到它并没有持有我们的 Widget 引用,而是持有一个 builder,而我们知道 builder 是一个方法参数,并没有实体,所以在显示的时候,肯定会调用它,我们看它是那里调用的。

OverlayStatebuild 方法中,为每一个 OverlayEntry 构建了一个 _OverlayEntryWidget 组件。

Flutter 必知必会系列 —— Navigator 的开始 Overlay _OverlayEntryWidget 也是 StatefulWidget 组件,它的逻辑封装在 _OverlayEntryWidgetState 中。

class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> {
  @override
  void initState() {
    super.initState();
    widget.entry._updateMounted(true);
  }

  @override
  void dispose() {
    widget.entry._updateMounted(false);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TickerMode(
      enabled: widget.tickerEnabled,
      child: widget.entry.builder(context),//第一处
    );
  }

  void _markNeedsBuild() {
    setState(() { /* the state that changed is in the builder */ });
  }
}

我们看第一处,这里调用了我们上面说的 builder 。所以,总结下来就是:每一个 OverlayEntry 都有一个 _OverlayEntryWidget 组件,屏幕上实际挂载的是 _OverlayEntryWidget 组件。

Flutter 必知必会系列 —— Navigator 的开始 Overlay

官方一值说自管理,自管理是什么呢?就是刷新和删除!!

具体的逻辑是这样的~

class OverlayEntry extends ChangeNotifier {

  //... 省略代码
  
  OverlayState? _overlay;//第一处
  final GlobalKey<_OverlayEntryWidgetState> _key = GlobalKey<_OverlayEntryWidgetState>(); //第一处

  void remove() { //第二处
    final OverlayState overlay = _overlay!;
    _overlay = null;
    if (!overlay.mounted)
      return;

    overlay._entries.remove(this);
    if (SchedulerBinding.instance!.schedulerPhase == SchedulerPhase.persistentCallbacks) {
      SchedulerBinding.instance!.addPostFrameCallback((Duration duration) {
        overlay._markDirty();
      });
    } else {
      overlay._markDirty();
    }
  }

  void markNeedsBuild() {//第三处
    _key.currentState?._markNeedsBuild();
  }
}

首先看第一处:_overlay_key 是非常巧妙的。对开发者来说,关闭就是 Overly 组件不显示某一个组件,开发者不引用 Overly,那脏活肯定得有人干,就是 OverlayEntry 来干的。

_overlay 就是显示悬浮的 OverlayOverlayState 引用,在插入的时候会为这个值赋值。

Flutter 必知必会系列 —— Navigator 的开始 Overlay

_key就是包裹的 _OverlayEntryWidget 的 key,而且还是 GlobalKey,这样就可以拿到包裹的组件,调用包裹组件的方法。

Flutter 必知必会系列 —— Navigator 的开始 Overlay

在 Overlay 的 build 方法中,为每个 OverlayEntry 生成 _OverlayEntryWidget 的时候,会用这个 key组件的 key 赋值。

所以 OverlayEntry 中持有了 Overlay 的实现逻辑 和 包裹组件的实现逻辑,在这个基础上实现,删除和刷新就很顺理成章了。

首先看删除,删除的逻辑在上面的第二处

void remove() {
  final OverlayState overlay = _overlay!;
  _overlay = null;
  if (!overlay.mounted)
    return;

  overlay._entries.remove(this);
  if (SchedulerBinding.instance!.schedulerPhase == SchedulerPhase.persistentCallbacks) {
    SchedulerBinding.instance!.addPostFrameCallback((Duration duration) {
      overlay._markDirty();
    });
  } else {
    overlay._markDirty();
  }
}

移除就比较简单了,overlay_entries 就是 OverlayEntry 集合,从集合中把自己删除,然后调用 overlay 的 _markDirty 方法,实质是 setState 方法刷新就可以了。这样悬浮窗就关闭了。

再看刷新,刷新就是 markNeedsBuild 方法

void markNeedsBuild() {
  _key.currentState?._markNeedsBuild();
}

_key.currentState 就是包裹的 _OverlayEntryWidget 的 State 对象,它的 _markNeedsBuild 就是 setState。

void _markNeedsBuild() {
 setState(() { /* the state that changed is in the builder */ });
}

这样我们给 OverlayEntry 的 builder 方法就会重新调用,从而实现刷新效果。 总结下来就是:

Flutter 必知必会系列 —— Navigator 的开始 Overlay

build 方法才可以显示

上面我们讲了 OverlayStatefulWidget,显示逻辑封装在 OverlayState 中,和其他的 StatefulWidget 一样,它的显示也在 build 中。

在分析 build 之前,我们先说一下概念:Overlay 其实并不会保持所有的 Entry,而是仅仅保持需要的 Entry

Overly 仅仅显示 不被上层遮挡(opaque = false)的

Overly 仅仅会刷新 存在内存中(maintainState = true)的

我们看是如何做到这一点的。

@override
Widget build(BuildContext context) {
  final List<Widget> children = <Widget>[]; 
  bool onstage = true;
  int onstageCount = 0;//第一处 
  for (int i = _entries.length - 1; i >= 0; i -= 1) {
    final OverlayEntry entry = _entries[i];
    if (onstage) {
      onstageCount += 1;
      children.add(_OverlayEntryWidget(
        key: entry._key,
        entry: entry,
      ));//第二处
      if (entry.opaque)
        onstage = false;//第三处
    } else if (entry.maintainState) {
      children.add(_OverlayEntryWidget(
        key: entry._key,
        entry: entry,
        tickerEnabled: false,
      ));//第二处 //第四处
    }
  }
  return _Theatre( //第五处
    skipCount: children.length - onstageCount,
    clipBehavior: widget.clipBehavior,
    children: children.reversed.toList(growable: false),
  );
}

_Theatre 舞台是 MultiChildRenderObjectWidget组件

在舞台上的(显示出来的)就是 onstage 的。不在舞台上的(不显示但是还活着的)就是 maintainState 的。

这里有opaque属性,opaque的意思是 完全不透明,在这里就是完全覆盖住了屏幕。

也就说,如果某一个人完全占据了舞台,那么其他的人 就上不去舞台了。

再看 这个人还能不能活,也就是 maintainState 属性是不是 true。如果ture的话,那就是不在舞台上,后面可能会上到舞台上。

把这些在舞台上人,放到 Stack 中,所以就可以显示啦。

我们看 build 的代码。

第一处的代码:先声明初始化的标记位onstage 默认是 true 的,也就是第一个是可以上舞台的。onstageCount 默认是 0 ,表示当前舞台上没有人。

第二处的代码:对在舞台上的Entry进行包装,上面我们已经分析过了。

第三处的代码:看从第几个人开始,不用等上舞台了。前一个 Entry 的opaque 属性是否是 true。如果前一个人完全占据了舞台,那么就走到了第四处。

第四处的代码:如果一个 entry,死皮赖脸不显示也要等待,那么也会添加到数组中,但是计数器不会 +1。除此之外,我们看 TickerMode 的 enabled 属性是 false,就是说里面的动画也不会执行。

看到这里我们会发现:children 数组的数量小于等于我们设置的 Entry 数组的数量。 因为有一波 entry 可能不是死皮赖脸的,它的 maintainState 是 false,就会被舍弃掉。

第五处的代码:把数组交给舞台_Theatre_Theatre 帮开发者处理了显示逻辑。

所以,结论就是 Overlay 其实并不会保持所有的 Entry,而是仅仅保持需要的 Entry 。只要一个 Entry 不透明,后续不想存活的 Entry,都不会挂载到树上。并且显示的逻辑是 _Theatre 实现的,我们暂且先知道 _Theatre 就是一个定制化的 Stack,鉴于篇幅下一篇专门介绍 _Theatre。

简单的插入和删除

Overlay 提供了两种插入的方式,插入单个 OverlayEntry 和 插入一组 OverlayEntry

Overlay.of(context)?.insert(entry); //插入单个

Overlay.of(context)?.insertAll(entries); //插入一组

上面我们知道 Overlay.of(context) 实际就是 OverlayState ,所以就走到它里面的插入方法。

插入单个

void insert(OverlayEntry entry, { OverlayEntry? below, OverlayEntry? above }) {
  entry._overlay = this; //第一处
  setState(() {
    _entries.insert(_insertionIndex(below, above), entry);//第二处
  });
}

entry 是想要插入的 OverlayEntry below 如果设置了,那么 OverlayEntry 会插在 below 下面 above 如果设置了,那么 OverlayEntry 会插在 above 上面

第一处就是我们上面提到的,为 OverlayEntry 绑定 OverlayState

第二处就是在数组中插入 OverlayEntry 并重新执行 build 方法(看上面的build 方法介绍哦)。这里有一点注意一下,_insertionIndex 会找到合适的位置,如果below 和 above 都不设置的话就是在数组的顶部插入,也就是显示的窗口的最前面~~~

插入多个

void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry? below, OverlayEntry? above }) {
  if (entries.isEmpty)
    return;
  for (final OverlayEntry entry in entries) {
    entry._overlay = this;
  }
  setState(() {
    _entries.insertAll(_insertionIndex(below, above), entries);
  });
}

和插入单个逻辑基本是一样的,就是为数组的每一个元素的都绑定了 OverlayState,也是 setState 刷新,也是为找到合适的位置。

删除我们上面介绍了,就是反向找到 OverlayEntry 绑定的 OverlayState,然后从数组中移出自己,也是 setState (overlay._markDirty())。

void remove() {
  final OverlayState overlay = _overlay!;
  _overlay = null;
  if (!overlay.mounted)
    return;

  overlay._entries.remove(this);
  if (SchedulerBinding.instance!.schedulerPhase == SchedulerPhase.persistentCallbacks) {
    SchedulerBinding.instance!.addPostFrameCallback((Duration duration) {
      overlay._markDirty();
    });
  } else {
    overlay._markDirty();
  }
}

唯一不同的是,多了一个帧调度。这里简单介绍一下,我们后面介绍初始化的时候,在详细介绍帧调度。整个帧分为一下几个阶段:

enum SchedulerPhase {
  /// 空闲
  idle,

  /// 动画
  transientCallbacks,

  /// 微任务
  midFrameMicrotasks,

  /// 布局绘制
  persistentCallbacks,

  /// 下一帧
  postFrameCallbacks,
}

在删除的时候,如果是在布局绘制阶段之前,那么就在当前帧把移除的任务完成,否则就安排到下一帧执行

所以总结下来就是,不管插入还是删除,都是变化了 OverlayState 维护的 OverlayEntry 数组,然后 setState,执行自己的 build 方法,在 build 方法中变化需要显示的 OverlayEntry 数组,交给 _Theatre 组件显示。

总结

至此,我们就认识了 Overlay ,并且可以使用 Overlay 实现指定位置、随意拖动的悬浮窗。再进一步,知道了 Overlay 就是 StatefulWidget,它的任务是管理 OverlayEntry 数组,每一个 OverlayEntry 是自管理的可以删除自己、刷新自己。真正负责布局承载的是 _Theatre 组件,下一篇我们就结合中的 OverlayEntry 的 opaquemaintainState 来专门揭秘它。

转载自:https://juejin.cn/post/7068164893672734750
评论
请登录