Overlay & OverlayEntry & OverlayPortal

Overlay

简单来讲,Overlay 可以实现悬浮功能。程序运行时,Overlay 可以随时将任意组件插入其内部 Stack 的最顶层,使之悬浮在所有其他组件之上。

[!tip] 通常无需自己创建 Overlay 组件,常用的 WidgetApp、MaterialApp 等都会隐式的包含了 Overlay 组件(由其内部 Navigator(导航器)创建)。

OverlayEntry 是一种命令式 API,需要通过 insertremove 方法对其生命周期进行实际管理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  final overlayEntry = OverlayEntry(
    // Potentially, a *root* context that doesn't
    // have an access to InheritedWidgets.
    builder: (BuildContext context) {
      return const Text('Hello, World!');
    },
  );

  // Insert the overlay using Overlay.of(context).insert().
  Overlay.of(context).insert(overlayEntry);

  // Remove the overlay using overlayEntry.remove().
  overlayEntry.remove();

 OverlayPortal

官方文档:

覆盖子级最初是隐藏的,直到在关联的 controller 上调用 OverlayPortalController.showOverlayPortal 使用 overlayChildBuilder 构建其覆盖子项,并将其呈现在指定的 Overlay 上,就好像它是使用 OverlayEntry 插入的一样,同时它可以依赖于同一组 InheritedWidget(例如 Theme)。

[!warning] 显示其 overlayChild 时,需要(依赖)小部件树中的 Overlay 祖先。

1
2
3
4
5
6
7
8
9
OverlayPortal(
	controller: widget.controller,
	overlayChildBuilder: (BuildContext context) {
	   return Align(
	       child: Text('Show'),
	    );
	},
	child: widget.child,
);

Example

  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
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import 'package:flutter/material.dart';

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

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final _infoPopupController = OverlayPortalController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: false,
        title: const Text('Upgrade Plan'),
        actions: [_HelpButton(_infoPopupController)],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Hello World'),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: _infoPopupController.toggle,
              child: const Text('Toggle Popup'),
            ),
          ],
        ),
      ),
    );
  }
}

class _HelpButton extends StatelessWidget {
  const _HelpButton(this.controller);

  final OverlayPortalController controller;

  @override
  Widget build(BuildContext context) {
    return Popup(
      follower: _HelpOverlay(controller.hide),
      followerAnchor: Alignment.topRight,
      targetAnchor: Alignment.topRight,
      controller: controller,
      child: OutlinedButton(
        style: OutlinedButton.styleFrom(
          padding: const EdgeInsets.symmetric(horizontal: 8),
        ),
        onPressed: controller.show,
        child: const Text('Help'),
      ),
    );
  }
}

class _HelpOverlay extends StatelessWidget {
  const _HelpOverlay(this.hide);

  final VoidCallback hide;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 200,
      child: Card(
        margin: EdgeInsets.zero,
        surfaceTintColor: Colors.white,
        elevation: 4,
        shape: const ContinuousRectangleBorder(
          borderRadius: BorderRadius.all(Radius.circular(28)),
        ),
        child: OutlinedButton(
          onPressed: hide,
          child: const Text('Close'),
        ),
      ),
    );
  }
}

/// A widget that shows a popup relative to a target widget.
///
/// The popup is declaratively shown/hidden using an [OverlayPortalController].
///
/// It is positioned relative to the target widget using the [followerAnchor] and [targetAnchor] properties.
class Popup extends StatefulWidget {
  const Popup({
    required this.child,
    required this.follower,
    required this.controller,
    this.offset = Offset.zero,
    this.followerAnchor = Alignment.topCenter,
    this.targetAnchor = Alignment.bottomCenter,
    super.key,
  });

  /// The target widget that will be used to position the follower widget.
  final Widget child;

  /// The widget that will be positioned relative to the target widget.
  final Widget follower;

  /// The controller that will be used to show/hide the overlay.
  final OverlayPortalController controller;

  /// The alignment of the follower widget relative to the target widget.
  ///
  /// Defaults to [Alignment.topCenter].
  final Alignment followerAnchor;

  /// The alignment of the target widget relative to the follower widget.
  ///
  /// Defaults to [Alignment.bottomCenter].
  final Alignment targetAnchor;

  /// The offset of the follower widget relative to the target widget.
  /// This is useful for fine-tuning the position of the follower widget.
  ///
  /// Defaults to [Offset.zero].
  final Offset offset;

  @override
  State<Popup> createState() => _PopupState();
}

class _PopupState extends State<Popup> {
  /// The link between the target widget and the follower widget.
  final _layerLink = LayerLink();

  @override
  Widget build(BuildContext context) {
    return CompositedTransformTarget(
      // Link the target widget to the follower widget.
      link: _layerLink,
      child: OverlayPortal(
        controller: widget.controller,
        child: widget.child,
        overlayChildBuilder: (BuildContext context) {
          // It is needed to wrap the follower widget in a widget that fills the space of the overlay.
          // This is needed to make sure that the follower widget is positioned relative to the target widget.
          // If not wrapped, the follower widget will fill the screen and be positioned incorrectly.
          return Align(
            child: CompositedTransformFollower(
              // Link the follower widget to the target widget.
              link: _layerLink,
              // The follower widget should not be shown when the link is broken.
              showWhenUnlinked: false,
              followerAnchor: widget.followerAnchor,
              targetAnchor: widget.targetAnchor,
              offset: widget.offset,
              child: widget.follower,
            ),
          );
        },
      ),
    );
  }
}

Referene

Licensed under CC BY-NC-SA 4.0