当前位置:   article > 正文

Flutter之使用overlay显示悬浮控件_overlayentry

overlayentry

Overlay与OverlayEntry

Overlay是一个Stack的widget,可以将overlay entry插入到overlay中,使独立的child窗口悬浮于其他widget之上。 因为Overlay本身使用的是[Stack]布局,所以overlay entry可以使用[Positioned] 或者 [AnimatedPositioned]在overlay中定位自己的位置。 当我们创建MaterialApp的时候,它会自动创建一个Navigator,然后创建一个Overlay; 然后利用这个Navigator来管理路由中的界面。 就我感觉,有点类似Android中的WindowManager,可以利用addView和removeView方法添加或删除View到界面中。

Overlay的使用方法

主要就是两个方法,往Overlay中插入entry,删除Overlay中的entry。

  1. //创建OverlayEntry
  2. Overlay entry=new OverlayEntry(builder:(){/*在这里创建对应的widget*/});
  3. //往Overlay中插入插入OverlayEntry
  4. Overlay.of(context).insert(overlayEntry);
  5. //调用entry自身的remove()方法,从所在的overlay中移除自己
  6. entry.remove();
  7. 复制代码

Overlay的使用场景

要将某个widget悬浮到页面顶部,就可以利用Overlay来实现。挺多场景都会用到,比如下面的例子。

自定义Toast

 

 

如果自己写插件调用原生的Toast的话,比较麻烦,可能还会出一些适配的问题。所以可以在Flutter中利用Overlay实现Toast的效果,比较方便,而且无需担心适配的问题。 下面是简单地显示一个Toast,更多功能的话,自行封装咯。

 

  1. /**
  2. * 利用overlay实现Toast
  3. */
  4. class Toast {
  5. static void show({@required BuildContext context, @required String message}) {
  6. //创建一个OverlayEntry对象
  7. OverlayEntry overlayEntry = new OverlayEntry(builder: (context) {
  8. //外层使用Positioned进行定位,控制在Overlay中的位置
  9. return new Positioned(
  10. top: MediaQuery.of(context).size.height * 0.7,
  11. child: new Material(
  12. child: new Container(
  13. width: MediaQuery.of(context).size.width,
  14. alignment: Alignment.center,
  15. child: new Center(
  16. child: new Card(
  17. child: new Padding(
  18. padding: EdgeInsets.all(8),
  19. child: new Text(message),
  20. ),
  21. color: Colors.grey,
  22. ),
  23. ),
  24. ),
  25. ));
  26. });
  27. //往Overlay中插入插入OverlayEntry
  28. Overlay.of(context).insert(overlayEntry);
  29. //两秒后,移除Toast
  30. new Future.delayed(Duration(seconds: 2)).then((value) {
  31. overlayEntry.remove();
  32. });
  33. }
  34. }
  35. 复制代码

类似PopupWindow的弹窗效果

 

 

 

比如实现微信首页右上角,点击“+”后的显示的弹窗效果。

(TODO:如何监听某个widget的焦点变化,我知道textform可以用focusNode来监听焦点变化,那其他widget如何监听焦点变化呢?)

  1. /**
  2. * 展示微信下拉的弹窗
  3. */
  4. void showWeixinButtonView() {
  5. weixinOverlayEntry = new OverlayEntry(builder: (context) {
  6. return new Positioned(
  7. top: kToolbarHeight,
  8. right: 20,
  9. width: 200,
  10. height: 320,
  11. child: new SafeArea(
  12. child: new Material(
  13. child: new Container(
  14. color: Colors.black,
  15. child: new Column(
  16. children: <Widget>[
  17. Expanded(
  18. child: new ListTile(
  19. leading: Icon(
  20. Icons.add,
  21. color: Colors.white,
  22. ),
  23. title: new Text(
  24. "发起群聊",
  25. style: TextStyle(color: Colors.white),
  26. ),
  27. ),
  28. ),
  29. Expanded(
  30. child: new ListTile(
  31. leading: Icon(Icons.add, color: Colors.white),
  32. title: new Text("添加朋友",
  33. style: TextStyle(color: Colors.white)),
  34. ),
  35. ),
  36. Expanded(
  37. child: new ListTile(
  38. leading: Icon(Icons.add, color: Colors.white),
  39. title: new Text("扫一扫",
  40. style: TextStyle(color: Colors.white)),
  41. ),
  42. ),
  43. Expanded(
  44. child: new ListTile(
  45. leading: Icon(Icons.add, color: Colors.white),
  46. title: new Text("首付款",
  47. style: TextStyle(color: Colors.white)),
  48. ),
  49. ),
  50. Expanded(
  51. child: new ListTile(
  52. leading: Icon(Icons.add, color: Colors.white),
  53. title: new Text("帮助与反馈",
  54. style: TextStyle(color: Colors.white)),
  55. ),
  56. ),
  57. ],
  58. ),
  59. ),
  60. )));
  61. });
  62. Overlay.of(context).insert(weixinOverlayEntry);
  63. }
  64. }
  65. 复制代码

比如,在某个TextForm获得焦点的时候,在TextForm下方显示一个listview的选择项

 

 

 

  1. FocusNode focusNode = new FocusNode();
  2. OverlayEntry overlayEntry;
  3. LayerLink layerLink = new LayerLink();
  4. @override
  5. void initState() {
  6. super.initState();
  7. focusNode.addListener(() {
  8. if (focusNode.hasFocus) {
  9. overlayEntry = createSelectPopupWindow();
  10. Overlay.of(context).insert(overlayEntry);
  11. } else {
  12. overlayEntry.remove();
  13. }
  14. });
  15. }
  16. /**
  17. * 利用Overlay实现PopupWindow效果,悬浮的widget
  18. * 利用CompositedTransformFollower和CompositedTransformTarget
  19. */
  20. OverlayEntry createSelectPopupWindow() {
  21. OverlayEntry overlayEntry = new OverlayEntry(builder: (context) {
  22. return new Positioned(
  23. width: 200,
  24. child: new CompositedTransformFollower(
  25. offset: Offset(0.0, 50),
  26. link: layerLink,
  27. child: new Material(
  28. child: new Container(
  29. color: Colors.grey,
  30. child: new Column(
  31. children: <Widget>[
  32. new ListTile(
  33. title: new Text("选项1"),
  34. onTap: () {
  35. Toast.show(context: context, message: "选择了选项1");
  36. focusNode.unfocus();
  37. },
  38. ),
  39. new ListTile(
  40. title: new Text("选项2"),
  41. onTap: () {
  42. Toast.show(context: context, message: "选择了选项1");
  43. focusNode.unfocus();
  44. }),
  45. ],
  46. )),
  47. ),
  48. ),
  49. );
  50. });
  51. return overlayEntry;
  52. }
  53. 复制代码

代码地址

github.com/LXD31256949…

Overlay的源码(涉及Render方面的暂时没理解,就没翻译了)

  1. //源文件:overlay.dart
  2. import 'dart:async';
  3. import 'dart:collection';
  4. import 'package:flutter/foundation.dart';
  5. import 'package:flutter/rendering.dart';
  6. import 'package:flutter/scheduler.dart';
  7. import 'basic.dart';
  8. import 'debug.dart';
  9. import 'framework.dart';
  10. import 'ticker_provider.dart';
  11. /// [Overlay]中的任意位置都可以包含一个widget。因为Overlay本身使用的是[Stack]布局,所以overlay entry
  12. /// 可以使用[Positioned] 或者 [AnimatedPositioned]在overlay中定位自己的位置。
  13. ///
  14. /// Overlay entries 使用[OverlayState.insert] or [OverlayState.insertAll]方法就可以插入到
  15. /// [Overlay]中。可以使用[Overlay.of]方法和利用所给的[BuildContext] 去找到最近的overlay实例并拿到OverlayState对象
  16. ///
  17. ///一个overlay entry在同一个时间只能被插入到最多一个overlay中。可以调用overlay entry自身的[remove]方法,去移除它所在的overlay
  18. ///
  19. ///
  20. /// For example, [Draggable] uses an [OverlayEntry] to show the drag avatar that
  21. /// follows the user's finger across the screen after the drag begins. Using the
  22. /// overlay to display the drag avatar lets the avatar float over the other
  23. /// widgets in the app. As the user's finger moves, draggable calls
  24. /// [markNeedsBuild] on the overlay entry to cause it to rebuild. It its build,
  25. /// the entry includes a [Positioned] with its top and left property set to
  26. /// position the drag avatar near the user's finger. When the drag is over,
  27. /// [Draggable] removes the entry from the overlay to remove the drag avatar
  28. /// from view.
  29. ///
  30. /// By default, if there is an entirely [opaque] entry over this one, then this
  31. /// one will not be included in the widget tree (in particular, stateful widgets
  32. /// within the overlay entry will not be instantiated). To ensure that your
  33. /// overlay entry is still built even if it is not visible, set [maintainState]
  34. /// to true. This is more expensive, so should be done with care. In particular,
  35. /// if widgets in an overlay entry with [maintainState] set to true repeatedly
  36. /// call [State.setState], the user's battery will be drained unnecessarily.
  37. class OverlayEntry {
  38. /// OverlayEntry的构造方法,
  39. ///
  40. /// 为了将overlay entry插入到 [Overlay]中, 首先是使用[Overlay.of]方法去拿到OverlayState对象,
  41. /// 然后再调用[OverlayState.insert]将overlay entry插入到overlay中。
  42. /// 调用overlay entry自身的[remove]方法,去移除它所在的overlay
  43. OverlayEntry({
  44. @required this.builder,
  45. bool opaque = false,
  46. bool maintainState = false,
  47. }) : assert(builder != null),
  48. assert(opaque != null),
  49. assert(maintainState != null),
  50. _opaque = opaque,
  51. _maintainState = maintainState;
  52. /// 通过builder进行创建对应的widget,并显示在overlay中的对应位置
  53. /// 如果想再次调用这个builder方法,需要调用overlay entry自身的[markNeedsBuild]方法
  54. final WidgetBuilder builder;
  55. /// Whether this entry occludes the entire overlay.
  56. ///
  57. /// If an entry claims to be opaque, then, for efficiency, the overlay will
  58. /// skip building entries below that entry unless they have [maintainState]
  59. /// set.
  60. bool get opaque => _opaque;
  61. bool _opaque;
  62. set opaque(bool value) {
  63. if (_opaque == value)
  64. return;
  65. _opaque = value;
  66. assert(_overlay != null);
  67. _overlay._didChangeEntryOpacity();
  68. }
  69. /// Whether this entry must be included in the tree even if there is a fully
  70. /// [opaque] entry above it.
  71. ///
  72. /// By default, if there is an entirely [opaque] entry over this one, then this
  73. /// one will not be included in the widget tree (in particular, stateful widgets
  74. /// within the overlay entry will not be instantiated). To ensure that your
  75. /// overlay entry is still built even if it is not visible, set [maintainState]
  76. /// to true. This is more expensive, so should be done with care. In particular,
  77. /// if widgets in an overlay entry with [maintainState] set to true repeatedly
  78. /// call [State.setState], the user's battery will be drained unnecessarily.
  79. ///
  80. /// 这个字段,是给[Navigator]和[Route]使用的,确保路由即使在后台也能维持状态。
  81. bool get maintainState => _maintainState;
  82. bool _maintainState;
  83. set maintainState(bool value) {
  84. assert(_maintainState != null);
  85. if (_maintainState == value)
  86. return;
  87. _maintainState = value;
  88. assert(_overlay != null);
  89. _overlay._didChangeEntryOpacity();
  90. }
  91. OverlayState _overlay;
  92. final GlobalKey<_OverlayEntryState> _key = GlobalKey<_OverlayEntryState>();
  93. /// 从overlay中移除overlay entry.
  94. /// 这个方法只能被调用一次
  95. void remove() {
  96. assert(_overlay != null);
  97. final OverlayState overlay = _overlay;
  98. _overlay = null;
  99. if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
  100. SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
  101. overlay._remove(this);
  102. });
  103. } else {
  104. overlay._remove(this);
  105. }
  106. }
  107. /// 需要在[builder]所创建的widget发生变化的时候,调用这个方法,会导致entry在下一次管道刷新期间进行rebuild操作。
  108. void markNeedsBuild() {
  109. _key.currentState?._markNeedsBuild();
  110. }
  111. @override
  112. String toString() => '${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)';
  113. }
  114. class _OverlayEntry extends StatefulWidget {
  115. _OverlayEntry(this.entry)
  116. : assert(entry != null),
  117. super(key: entry._key);
  118. final OverlayEntry entry;
  119. @override
  120. _OverlayEntryState createState() => _OverlayEntryState();
  121. }
  122. class _OverlayEntryState extends State<_OverlayEntry> {
  123. @override
  124. Widget build(BuildContext context) {
  125. return widget.entry.builder(context);
  126. }
  127. void _markNeedsBuild() {
  128. setState(() { /* the state that changed is in the builder */ });
  129. }
  130. }
  131. /// A [Stack] of entries that can be managed independently.
  132. /// Overlay是一个[Stack]布局,可以独立管理存放在它里面的overlay entry。
  133. ///
  134. /// 通过将entry插入到overlay的[Stack]中,Overlay可以让child widget悬浮于其他可视化的widgets上面。
  135. /// 虽然你可以直接创建一个[Overlay],但是最常见的用法,是在[WidgetsApp]或者[MaterialApp]中使用由Navigator创建的overlay对象就行。
  136. /// Navigator的原理,是使用它的overlay来管理路由中的可视化界面。
  137. class Overlay extends StatefulWidget {
  138. /// Overlay的构造方法.
  139. ///
  140. /// 在与它所关联的[OverlayState]被初始化后,initialEntries被插入到overlay中。
  141. /// 与其自己创建一个overlay,比如考虑直接使用由[WidgetsApp]或者 [MaterialApp]为应用程序所创建的overlay就行。
  142. const Overlay({
  143. Key key,
  144. this.initialEntries = const <OverlayEntry>[]
  145. }) : assert(initialEntries != null),
  146. super(key: key);
  147. /// 这些entries是overlay初始化的时候,被会插入到overlay中的entry.
  148. /// 插入方法和删除方法,跟上面讲的是一样的。
  149. /// 使用[Overlay.of]方法去拿到OverlayState对象,
  150. /// 然后再调用[OverlayState.insert]将overlay entry插入到overlay中。
  151. /// 调用overlay entry自身的[remove]方法,去移除它所在的overlay
  152. final List<OverlayEntry> initialEntries;
  153. /// The state from the closest instance of this class that encloses the given context.
  154. /// 通过所传的context上下文对象,利用context.ancestorStateOfType方法,返回与给定state类型匹配的最接近的祖先小部件State对象
  155. /// 这个其实是InheritedWidget的原理,利用context.ancestorStateOfType方法,找到应用程序中的OverlayState祖先对象。
  156. /// 使用方法:
  157. /// ```dart
  158. /// OverlayState overlay = Overlay.of(context);
  159. /// ```
  160. static OverlayState of(BuildContext context, { Widget debugRequiredFor }) {
  161. final OverlayState result = context.ancestorStateOfType(const TypeMatcher<OverlayState>());
  162. assert(() {
  163. if (debugRequiredFor != null && result == null) {
  164. final String additional = context.widget != debugRequiredFor
  165. ? '\nThe context from which that widget was searching for an overlay was:\n $context'
  166. : '';
  167. throw FlutterError(
  168. 'No Overlay widget found.\n'
  169. '${debugRequiredFor.runtimeType} widgets require an Overlay widget ancestor for correct operation.\n'
  170. 'The most common way to add an Overlay to an application is to include a MaterialApp or Navigator widget in the runApp() call.\n'
  171. 'The specific widget that failed to find an overlay was:\n'
  172. ' $debugRequiredFor'
  173. '$additional'
  174. );
  175. }
  176. return true;
  177. }());
  178. return result;
  179. }
  180. @override
  181. OverlayState createState() => OverlayState();
  182. }
  183. /// [Overlay]当前的State对象
  184. /// 可以用来插入overlay entry到overlay中
  185. class OverlayState extends State<Overlay> with TickerProviderStateMixin {
  186. final List<OverlayEntry> _entries = <OverlayEntry>[];
  187. @override
  188. void initState() {
  189. super.initState();
  190. insertAll(widget.initialEntries);
  191. }
  192. /// 把给定的entry插入到overlay中
  193. /// 如果[above]不为空,则entry会被插入到[above]上面。默认,一般是直接插入到最顶部。
  194. void insert(OverlayEntry entry, { OverlayEntry above }) {
  195. assert(entry._overlay == null);
  196. assert(above == null || (above._overlay == this && _entries.contains(above)));
  197. entry._overlay = this;
  198. setState(() {
  199. final int index = above == null ? _entries.length : _entries.indexOf(above) + 1;
  200. _entries.insert(index, entry);
  201. });
  202. }
  203. /// 把多个entry插入到overlay中
  204. void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry above }) {
  205. assert(above == null || (above._overlay == this && _entries.contains(above)));
  206. if (entries.isEmpty)
  207. return;
  208. for (OverlayEntry entry in entries) {
  209. assert(entry._overlay == null);
  210. entry._overlay = this;
  211. }
  212. setState(() {
  213. final int index = above == null ? _entries.length : _entries.indexOf(above) + 1;
  214. _entries.insertAll(index, entries);
  215. });
  216. }
  217. void _remove(OverlayEntry entry) {
  218. if (mounted) {
  219. _entries.remove(entry);
  220. setState(() { /* entry was removed */ });
  221. }
  222. }
  223. /// (DEBUG ONLY) Check whether a given entry is visible (i.e., not behind an
  224. /// opaque entry).
  225. ///
  226. /// This is an O(N) algorithm, and should not be necessary except for debug
  227. /// asserts. To avoid people depending on it, this function is implemented
  228. /// only in checked mode.
  229. bool debugIsVisible(OverlayEntry entry) {
  230. bool result = false;
  231. assert(_entries.contains(entry));
  232. assert(() {
  233. for (int i = _entries.length - 1; i > 0; i -= 1) {
  234. final OverlayEntry candidate = _entries[i];
  235. if (candidate == entry) {
  236. result = true;
  237. break;
  238. }
  239. if (candidate.opaque)
  240. break;
  241. }
  242. return true;
  243. }());
  244. return result;
  245. }
  246. void _didChangeEntryOpacity() {
  247. setState(() {
  248. // We use the opacity of the entry in our build function, which means we
  249. // our state has changed.
  250. });
  251. }
  252. @override
  253. Widget build(BuildContext context) {
  254. // These lists are filled backwards. For the offstage children that
  255. // does not matter since they aren't rendered, but for the onstage
  256. // children we reverse the list below before adding it to the tree.
  257. final List<Widget> onstageChildren = <Widget>[];
  258. final List<Widget> offstageChildren = <Widget>[];
  259. bool onstage = true;
  260. for (int i = _entries.length - 1; i >= 0; i -= 1) {
  261. final OverlayEntry entry = _entries[i];
  262. if (onstage) {
  263. onstageChildren.add(_OverlayEntry(entry));
  264. if (entry.opaque)
  265. onstage = false;
  266. } else if (entry.maintainState) {
  267. offstageChildren.add(TickerMode(enabled: false, child: _OverlayEntry(entry)));
  268. }
  269. }
  270. return _Theatre(
  271. onstage: Stack(
  272. fit: StackFit.expand,
  273. children: onstageChildren.reversed.toList(growable: false),
  274. ),
  275. offstage: offstageChildren,
  276. );
  277. }
  278. @override
  279. void debugFillProperties(DiagnosticPropertiesBuilder properties) {
  280. super.debugFillProperties(properties);
  281. // TODO(jacobr): use IterableProperty instead as that would
  282. // provide a slightly more consistent string summary of the List.
  283. properties.add(DiagnosticsProperty<List<OverlayEntry>>('entries', _entries));
  284. }
  285. }


作者:入魔的冬瓜
链接:https://juejin.im/post/5c20cc086fb9a04a016457e2
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/酷酷是懒虫/article/detail/736415
推荐阅读
相关标签
  

闽ICP备14008679号