Flutter 实现全局气泡

认识 Overlay

看一下官方文档怎么介绍

arduino 复制代码
/// A stack of entries that can be managed independently.
///
/// 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.
///
/// 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.
///
/// 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.
///
/// See also:
///
///  * [OverlayEntry], the class that is used for describing the overlay entries.
///  * [OverlayState], which is used to insert the entries into the overlay.
///  * [WidgetsApp], which inserts an [Overlay] widget indirectly via its [Navigator].
///  * [MaterialApp], which inserts an [Overlay] widget indirectly via its [Navigator].
///  * [Stack], which allows directly displaying a stack of widgets.

翻译一下

css 复制代码
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的信息,请参考:说说Flutter中的OverlayOverlay原理源码解析部分

认识 OverlayState

dart 复制代码
  /// The current state of an [Overlay].
  ///
  /// Used to insert [OverlayEntry]s into the overlay using the [insert] and
  /// [insertAll] functions.
  class OverlayState extends State<Overlay> with TickerProviderStateMixin {

  }

  /// Insert the given entry into the overlay.
  ///
  /// If `below` is non-null, the entry is inserted just below `below`.
  /// If `above` is non-null, the entry is inserted just above `above`.
  /// Otherwise, the entry is inserted on top.
  ///
  /// It is an error to specify both `above` and `below`.
  void insert(OverlayEntry entry, { OverlayEntry? below, OverlayEntry? above }) {
  
  }
  
  /// Insert all the entries in the given iterable.
  ///
  /// If `below` is non-null, the entries are inserted just below `below`.
  /// If `above` is non-null, the entries are inserted just above `above`.
  /// Otherwise, the entries are inserted on top.
  ///
  /// It is an error to specify both `above` and `below`.
  void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry? below, OverlayEntry? above }) {
  
  }
  
  /// Remove all the entries listed in the given iterable, then reinsert them
  /// into the overlay in the given order.
  ///
  /// Entries mention in `newEntries` but absent from the overlay are inserted
  /// as if with [insertAll].
  ///
  /// Entries not mentioned in `newEntries` but present in the overlay are
  /// positioned as a group in the resulting list relative to the entries that
  /// were moved, as specified by one of `below` or `above`, which, if
  /// specified, must be one of the entries in `newEntries`:
  ///
  /// If `below` is non-null, the group is positioned just below `below`.
  /// If `above` is non-null, the group is positioned just above `above`.
  /// Otherwise, the group is left on top, with all the rearranged entries
  /// below.
  ///
  /// It is an error to specify both `above` and `below`.
  void rearrange(Iterable<OverlayEntry> newEntries, { OverlayEntry? below, OverlayEntry? above }) {
  
  }

认识OverlayEntry

先看一下系统对这个类的介绍

less 复制代码
/// A place in an [Overlay] that can contain a 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.
///
/// 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.
///
/// Because an [Overlay] uses a [Stack] layout, overlay entries can use
/// [Positioned] and [AnimatedPositioned] to position themselves within the
/// overlay.
///
/// For example, [Draggable] uses an [OverlayEntry] to show the drag avatar that
/// follows the user's finger across the screen after the drag begins. Using the
/// overlay to display the drag avatar lets the avatar float over the other
/// widgets in the app. As the user's finger moves, draggable calls
/// [markNeedsBuild] on the overlay entry to cause it to rebuild. In its build,
/// the entry includes a [Positioned] with its top and left property set to
/// position the drag avatar near the user's finger. When the drag is over,
/// [Draggable] removes the entry from the overlay to remove the drag avatar
/// from view.
///
/// By default, if there is an entirely [opaque] entry over this one, then this
/// one will not be included in the widget tree (in particular, stateful widgets
/// within the overlay entry will not be instantiated). To ensure that your
/// overlay entry is still built even if it is not visible, set [maintainState]
/// to true. This is more expensive, so should be done with care. In particular,
/// if widgets in an overlay entry with [maintainState] set to true repeatedly
/// call [State.setState], the user's battery will be drained unnecessarily.
///
/// [OverlayEntry] is a [ChangeNotifier] that notifies when the widget built by
/// [builder] is mounted or unmounted, whose exact state can be queried by
/// [mounted].
///
/// See also:
///
///  * [Overlay]
///  * [OverlayState]
///  * [WidgetsApp]
///  * [MaterialApp]

翻译一下

css 复制代码
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 来显示在指定的位置 opaque 和 maintainState 属性影响着三棵树的形成

再看类的定义,类继承自ChangeNotifier,构造只需一个参数builder

dart 复制代码
class OverlayEntry extends ChangeNotifier {
  /// Creates an overlay entry.
  ///
  /// To insert the entry into an [Overlay], first find the overlay using
  /// [Overlay.of] and then call [OverlayState.insert]. To remove the entry,
  /// call [remove] on the overlay entry itself.
 OverlayEntry OverlayEntry({
   required Widget Function(BuildContext) builder,
   bool opaque = false,
   bool maintainState = false,
 })

只定义了一个方法,就是从 overlay 中移除 entry

csharp 复制代码
  /// Remove this entry from the overlay.
  ///
  /// This should only be called once.
  ///
  /// This method removes this overlay entry from the overlay immediately. The
  /// UI will be updated in the same frame if this method is called before the
  /// overlay rebuild in this frame; otherwise, the UI will be updated in the
  /// next frame. This means that it is safe to call during builds, but also
  /// that if you do call this after the overlay rebuild, the UI will not update
  /// until the next frame (i.e. many milliseconds later).
  void remove()

封装一个气泡控件

思路:

  1. 通过overlayEntry可以在flutter页面的最顶层插入一个控件
  2. 通过Draggable控件实现控件和手势滑动的绑定

为了方便调用,把这个控件的调用方法声明为静态

dart 复制代码
class TestOverLay {
  static OverlayEntry? _holder;

  static Widget? view;
    
  // 移除方法
  static void remove() {
    if (_holder != null) {
      _holder?.remove();
      _holder = null;
    }
  }
}

实现显示方法

dart 复制代码
import 'package:flutter/material.dart';

class TestOverLay {
  static OverlayEntry? _holder;

  static Widget? view;

  static void remove() {
    if (_holder != null) {
      _holder?.remove();
      _holder = null;
    }
  }

  static void show({required BuildContext context, required Widget view}) {
    TestOverLay.view = view;

    remove();
    // 创建一个OverlayEntry对象
    OverlayEntry overlayEntry = OverlayEntry(builder: (context) {
      return Positioned(
          top: MediaQuery.of(context).size.height * 0.7,
          child: _buildDraggable(context));
    });

    // 往Overlay中插入插入OverlayEntry
    Overlay.of(context)!.insert(overlayEntry);

    _holder = overlayEntry;
  }

  static _buildDraggable(context) {
    if (view == null) {
      return;
    }

    return Draggable(
      child: view!,
      feedback: view!,
      onDragStarted: () {
        debugPrint('onDragStarted:');
      },
      onDragEnd: (detail) {
        debugPrint('onDragEnd:${detail.offset}');
        createDragTarget(offset: detail.offset, context: context);
      },
      childWhenDragging: Container(),
    );
  }

  static void createDragTarget(
      {required Offset offset, required BuildContext context}) {
    if (_holder != null) {
      _holder?.remove();
    }

    _holder = OverlayEntry(builder: (context) {
      bool isLeft = true;
      if (offset.dx + 100 > MediaQuery.of(context).size.width / 2) {
        isLeft = false;
      }

      double maxY = MediaQuery.of(context).size.height - 100;

      return Positioned(
          top: offset.dy < 50
              ? 50
              : offset.dy < maxY
                  ? offset.dy
                  : maxY,
          left: isLeft ? 0 : null,
          right: isLeft ? null : 0,
          child: DragTarget(
            onWillAccept: (data) {
              debugPrint('onWillAccept: $data');
              return true;
            },
            onAccept: (data) {
              debugPrint('onAccept: $data');
            },
            onLeave: (data) {
              debugPrint('onLeave: $data');
            },
            builder: (BuildContext context, List incoming, List rejected) {
              return _buildDraggable(context);
            },
          ));
    });
    Overlay.of(context)?.insert(_holder!);
  }
}

如何使用

在浮层上添加 A B C 3个按钮为例

dart 复制代码
import 'package:flutter/material.dart';

import 'package:flutter_demo_1/test_overlay.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  void showTestOverLay() {
    WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
      TestOverLay.show(
        context: context,
        view: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Column(children: [
            ElevatedButton(
              onPressed: () {
                debugPrint("on tap A");
                List<int> data = [1, 2, 3];
                int? _valueToBePrinted;

                for (var value in data) {
                  if (value == 2) {
                    _valueToBePrinted = value;
                    return;
                  }
                }
                // you can return something here to
                // return _valueToBePrinted;
                print(_valueToBePrinted);
              },
              child: const Text('A'),
            ),
            ElevatedButton(
              onPressed: () {
                debugPrint("on tap B");
              },
              child: const Text('B'),
            ),
            ElevatedButton(
              onPressed: () {
                debugPrint("on tap C");
              },
              child: const Text('C'),
            ),
          ]),
        ),
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    showTestOverLay();
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
    );
  }
}

拖拽一下看一下日志

yaml 复制代码
Performing hot reload...                                                
Reloaded 0 libraries in 52ms.
// 第一次
flutter: onDragStarted:
flutter: onDragEnd:Offset(337.0, 382.8)

// 第二次
flutter: onWillAccept: null
flutter: onDragStarted:
flutter: onLeave
flutter: onDragEnd:Offset(314.7, 360.5)

UI效果

视频演示

总结

Overlay 是啥?(What)

sql 复制代码
Overlay 中维护了一个 OverlayEntry 栈,并且每一个 OverlayEntry 是自管理的
Overlay 让它栈中的 Widget 悬浮在屏幕的其他组件之上。并且 OverlayEntry 可以实现悬浮组件
的自管理,比如插入、移出等等

我们可以通过OverlayEntry对象的配置来管理Overlay的层级关系。

系统那些地方使用了 Overlay?(Where)

diff 复制代码
- ScaffoldMessenger
- Navigator
...

Overlay 怎么用?(How)

markdown 复制代码
Overlay 的应用场景:
- 实现悬浮窗口的功能
  - flutter全局浮窗(全局隐藏页面的快速入口)
  - Toast
- 实现页面叠加(页面导航)
- 弹窗跟随滚动 (Overlay+ CompositedTransformFollower+CompositedTransformTarget) 具体示例参考这里
...


和Stack对比:
Overlay 和 Stack 非常相似,Overlay 的应用场景主要是:路由管理和悬浮窗口。其他的场景可以
直接使用 Stack 组件

Overlay 相关文章

相关推荐
Jinkey1 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
Summer不秃6 小时前
Flutter之使用mqtt进行连接和信息传输的使用案例
前端·flutter
旭日猎鹰6 小时前
Flutter踩坑记录(二)-- GestureDetector+Expanded点击无效果
前端·javascript·flutter
sunly_6 小时前
Flutter:AnimatedSwitcher当子元素改变时,触发动画
flutter
AiFlutter6 小时前
Flutter封装Coap
flutter
旭日猎鹰11 小时前
Flutter踩坑记录(三)-- 更改入口执行文件
flutter
旭日猎鹰11 小时前
Flutter踩坑记录(一)debug运行生成的项目,不能手动点击运行
flutter
️ 邪神11 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】自定义View
flutter·ios·鸿蒙·reactnative·anroid
比格丽巴格丽抱1 天前
flutter项目苹果编译运行打包上线
flutter·ios
SoaringHeart1 天前
Flutter进阶:基于 MLKit 的 OCR 文字识别
前端·flutter