Flutter之使用overlay顯示懸浮控件

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到界面中。git

Overlay的使用方法

主要就是兩個方法,往Overlay中插入entry,刪除Overlay中的entry。github

//建立OverlayEntry
Overlay entry=new OverlayEntry(builder:(){/*在這裏建立對應的widget*/});
//往Overlay中插入插入OverlayEntry
Overlay.of(context).insert(overlayEntry);
//調用entry自身的remove()方法,從所在的overlay中移除本身
entry.remove();
複製代碼

Overlay的使用場景

要將某個widget懸浮到頁面頂部,就能夠利用Overlay來實現。挺多場景都會用到,好比下面的例子。bash

自定義Toast

若是本身寫插件調用原生的Toast的話,比較麻煩,可能還會出一些適配的問題。因此能夠在Flutter中利用Overlay實現Toast的效果,比較方便,並且無需擔憂適配的問題。 下面是簡單地顯示一個Toast,更多功能的話,自行封裝咯。

/**
 * 利用overlay實現Toast
 */
class Toast {
  static void show({@required BuildContext context, @required String message}) {
    //建立一個OverlayEntry對象
    OverlayEntry overlayEntry = new OverlayEntry(builder: (context) {
    //外層使用Positioned進行定位,控制在Overlay中的位置
      return new Positioned(
          top: MediaQuery.of(context).size.height * 0.7,
          child: new Material(
            child: new Container(
              width: MediaQuery.of(context).size.width,
              alignment: Alignment.center,
              child: new Center(
                child: new Card(
                  child: new Padding(
                    padding: EdgeInsets.all(8),
                    child: new Text(message),
                  ),
                  color: Colors.grey,
                ),
              ),
            ),
          ));
    });
    //往Overlay中插入插入OverlayEntry
    Overlay.of(context).insert(overlayEntry);
    //兩秒後,移除Toast
    new Future.delayed(Duration(seconds: 2)).then((value) {
      overlayEntry.remove();
    });
  }
}
複製代碼

相似PopupWindow的彈窗效果

好比實現微信首頁右上角,點擊「+」後的顯示的彈窗效果。微信

(TODO:如何監聽某個widget的焦點變化,我知道textform能夠用focusNode來監聽焦點變化,那其餘widget如何監聽焦點變化呢?)app

/**
   * 展現微信下拉的彈窗
   */
  void showWeixinButtonView() {
    weixinOverlayEntry = new OverlayEntry(builder: (context) {
      return new Positioned(
          top: kToolbarHeight,
          right: 20,
          width: 200,
          height: 320,
          child: new SafeArea(
              child: new Material(
            child: new Container(
              color: Colors.black,
              child: new Column(
                children: <Widget>[
                  Expanded(
                    child: new ListTile(
                      leading: Icon(
                        Icons.add,
                        color: Colors.white,
                      ),
                      title: new Text(
                        "發起羣聊",
                        style: TextStyle(color: Colors.white),
                      ),
                    ),
                  ),
                  Expanded(
                    child: new ListTile(
                      leading: Icon(Icons.add, color: Colors.white),
                      title: new Text("添加朋友",
                          style: TextStyle(color: Colors.white)),
                    ),
                  ),
                  Expanded(
                    child: new ListTile(
                      leading: Icon(Icons.add, color: Colors.white),
                      title: new Text("掃一掃",
                          style: TextStyle(color: Colors.white)),
                    ),
                  ),
                  Expanded(
                    child: new ListTile(
                      leading: Icon(Icons.add, color: Colors.white),
                      title: new Text("首付款",
                          style: TextStyle(color: Colors.white)),
                    ),
                  ),
                  Expanded(
                    child: new ListTile(
                      leading: Icon(Icons.add, color: Colors.white),
                      title: new Text("幫助與反饋",
                          style: TextStyle(color: Colors.white)),
                    ),
                  ),
                ],
              ),
            ),
          )));
    });
    Overlay.of(context).insert(weixinOverlayEntry);
  }
}
複製代碼

好比,在某個TextForm得到焦點的時候,在TextForm下方顯示一個listview的選擇項

FocusNode focusNode = new FocusNode();
  OverlayEntry overlayEntry;

  LayerLink layerLink = new LayerLink();

  @override
  void initState() {
    super.initState();
    focusNode.addListener(() {
      if (focusNode.hasFocus) {
        overlayEntry = createSelectPopupWindow();
        Overlay.of(context).insert(overlayEntry);
      } else {
        overlayEntry.remove();
      }
    });
  }


  /**
     * 利用Overlay實現PopupWindow效果,懸浮的widget
     * 利用CompositedTransformFollower和CompositedTransformTarget
     */
    OverlayEntry createSelectPopupWindow() {
      OverlayEntry overlayEntry = new OverlayEntry(builder: (context) {
        return new Positioned(
          width: 200,
          child: new CompositedTransformFollower(
            offset: Offset(0.0, 50),
            link: layerLink,
            child: new Material(
              child: new Container(
                  color: Colors.grey,
                  child: new Column(
                    children: <Widget>[
                      new ListTile(
                        title: new Text("選項1"),
                        onTap: () {
                          Toast.show(context: context, message: "選擇了選項1");
                          focusNode.unfocus();
                        },
                      ),
                      new ListTile(
                          title: new Text("選項2"),
                          onTap: () {
                            Toast.show(context: context, message: "選擇了選項1");
                            focusNode.unfocus();
                          }),
                    ],
                  )),
            ),
          ),
        );
      });
      return overlayEntry;
    }


複製代碼

代碼地址

github.com/LXD31256949…less

Overlay的源碼(涉及Render方面的暫時沒理解,就沒翻譯了)

//源文件:overlay.dart

import 'dart:async';
import 'dart:collection';

import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';

import 'basic.dart';
import 'debug.dart';
import 'framework.dart';
import 'ticker_provider.dart';

/// [Overlay]中的任意位置均可以包含一個widget。由於Overlay自己使用的是[Stack]佈局,因此overlay entry
/// 可使用[Positioned] 或者 [AnimatedPositioned]在overlay中定位本身的位置。
///
/// Overlay entries 使用[OverlayState.insert] or [OverlayState.insertAll]方法就能夠插入到
/// [Overlay]中。可使用[Overlay.of]方法和利用所給的[BuildContext] 去找到最近的overlay實例並拿到OverlayState對象
///
///一個overlay entry在同一個時間只能被插入到最多一個overlay中。能夠調用overlay entry自身的[remove]方法,去移除它所在的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. It 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.

class OverlayEntry {
  /// OverlayEntry的構造方法,
  ///
  /// 爲了將overlay entry插入到 [Overlay]中, 首先是使用[Overlay.of]方法去拿到OverlayState對象,
  /// 而後再調用[OverlayState.insert]將overlay entry插入到overlay中。
  /// 調用overlay entry自身的[remove]方法,去移除它所在的overlay
  OverlayEntry({
    @required this.builder,
    bool opaque = false,
    bool maintainState = false,
  }) : assert(builder != null),
       assert(opaque != null),
       assert(maintainState != null),
       _opaque = opaque,
       _maintainState = maintainState;


  /// 經過builder進行建立對應的widget,並顯示在overlay中的對應位置
  /// 若是想再次調用這個builder方法,須要調用overlay entry自身的[markNeedsBuild]方法
  final WidgetBuilder builder;

  /// Whether this entry occludes the entire overlay.
  ///
  /// If an entry claims to be opaque, then, for efficiency, the overlay will
  /// skip building entries below that entry unless they have [maintainState]
  /// set.
  bool get opaque => _opaque;
  bool _opaque;
  set opaque(bool value) {
    if (_opaque == value)
      return;
    _opaque = value;
    assert(_overlay != null);
    _overlay._didChangeEntryOpacity();
  }

  /// Whether this entry must be included in the tree even if there is a fully
  /// [opaque] entry above it.
  ///
  /// 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. /// /// 這個字段,是給[Navigator]和[Route]使用的,確保路由即便在後臺也能維持狀態。 bool get maintainState => _maintainState; bool _maintainState; set maintainState(bool value) { assert(_maintainState != null); if (_maintainState == value) return; _maintainState = value; assert(_overlay != null); _overlay._didChangeEntryOpacity(); } OverlayState _overlay; final GlobalKey<_OverlayEntryState> _key = GlobalKey<_OverlayEntryState>(); /// 從overlay中移除overlay entry. /// 這個方法只能被調用一次 void remove() { assert(_overlay != null); final OverlayState overlay = _overlay; _overlay = null; if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) { SchedulerBinding.instance.addPostFrameCallback((Duration duration) { overlay._remove(this); }); } else { overlay._remove(this); } } /// 須要在[builder]所建立的widget發生變化的時候,調用這個方法,會致使entry在下一次管道刷新期間進行rebuild操做。 void markNeedsBuild() { _key.currentState?._markNeedsBuild(); } @override String toString() => '${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)'; } class _OverlayEntry extends StatefulWidget { _OverlayEntry(this.entry) : assert(entry != null), super(key: entry._key); final OverlayEntry entry; @override _OverlayEntryState createState() => _OverlayEntryState(); } class _OverlayEntryState extends State<_OverlayEntry> { @override Widget build(BuildContext context) { return widget.entry.builder(context); } void _markNeedsBuild() { setState(() { /* the state that changed is in the builder */ }); } } /// A [Stack] of entries that can be managed independently. /// Overlay是一個[Stack]佈局,能夠獨立管理存放在它裏面的overlay entry。 /// /// 經過將entry插入到overlay的[Stack]中,Overlay可讓child widget懸浮於其餘可視化的widgets上面。 /// 雖然你能夠直接建立一個[Overlay],可是最多見的用法,是在[WidgetsApp]或者[MaterialApp]中使用由Navigator建立的overlay對象就行。 /// Navigator的原理,是使用它的overlay來管理路由中的可視化界面。 class Overlay extends StatefulWidget { /// Overlay的構造方法. /// /// 在與它所關聯的[OverlayState]被初始化後,initialEntries被插入到overlay中。 /// 與其本身建立一個overlay,好比考慮直接使用由[WidgetsApp]或者 [MaterialApp]爲應用程序所建立的overlay就行。 const Overlay({ Key key, this.initialEntries = const <OverlayEntry>[] }) : assert(initialEntries != null), super(key: key); /// 這些entries是overlay初始化的時候,被會插入到overlay中的entry. /// 插入方法和刪除方法,跟上面講的是同樣的。 /// 使用[Overlay.of]方法去拿到OverlayState對象, /// 而後再調用[OverlayState.insert]將overlay entry插入到overlay中。 /// 調用overlay entry自身的[remove]方法,去移除它所在的overlay final List<OverlayEntry> initialEntries; /// The state from the closest instance of this class that encloses the given context. /// 經過所傳的context上下文對象,利用context.ancestorStateOfType方法,返回與給定state類型匹配的最接近的祖先小部件State對象 /// 這個實際上是InheritedWidget的原理,利用context.ancestorStateOfType方法,找到應用程序中的OverlayState祖先對象。 /// 使用方法: /// ```dart /// OverlayState overlay = Overlay.of(context); /// ``` static OverlayState of(BuildContext context, { Widget debugRequiredFor }) { final OverlayState result = context.ancestorStateOfType(const TypeMatcher<OverlayState>()); assert(() { if (debugRequiredFor != null && result == null) { final String additional = context.widget != debugRequiredFor ? '\nThe context from which that widget was searching for an overlay was:\n  $context' : ''; throw FlutterError( 'No Overlay widget found.\n' '${debugRequiredFor.runtimeType} widgets require an Overlay widget ancestor for correct operation.\n' 'The most common way to add an Overlay to an application is to include a MaterialApp or Navigator widget in the runApp() call.\n' 'The specific widget that failed to find an overlay was:\n' '  $debugRequiredFor' '$additional' ); } return true; }()); return result; } @override OverlayState createState() => OverlayState(); } /// [Overlay]當前的State對象 /// 能夠用來插入overlay entry到overlay中 class OverlayState extends State<Overlay> with TickerProviderStateMixin { final List<OverlayEntry> _entries = <OverlayEntry>[]; @override void initState() { super.initState(); insertAll(widget.initialEntries); } /// 把給定的entry插入到overlay中 /// 若是[above]不爲空,則entry會被插入到[above]上面。默認,通常是直接插入到最頂部。 void insert(OverlayEntry entry, { OverlayEntry above }) { assert(entry._overlay == null); assert(above == null || (above._overlay == this && _entries.contains(above))); entry._overlay = this; setState(() { final int index = above == null ? _entries.length : _entries.indexOf(above) + 1; _entries.insert(index, entry); }); } /// 把多個entry插入到overlay中 void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry above }) { assert(above == null || (above._overlay == this && _entries.contains(above))); if (entries.isEmpty) return; for (OverlayEntry entry in entries) { assert(entry._overlay == null); entry._overlay = this; } setState(() { final int index = above == null ? _entries.length : _entries.indexOf(above) + 1; _entries.insertAll(index, entries); }); } void _remove(OverlayEntry entry) { if (mounted) { _entries.remove(entry); setState(() { /* entry was removed */ }); } } /// (DEBUG ONLY) Check whether a given entry is visible (i.e., not behind an /// opaque entry). /// /// This is an O(N) algorithm, and should not be necessary except for debug /// asserts. To avoid people depending on it, this function is implemented /// only in checked mode. bool debugIsVisible(OverlayEntry entry) { bool result = false; assert(_entries.contains(entry)); assert(() { for (int i = _entries.length - 1; i > 0; i -= 1) { final OverlayEntry candidate = _entries[i]; if (candidate == entry) { result = true; break; } if (candidate.opaque) break; } return true; }()); return result; } void _didChangeEntryOpacity() { setState(() { // We use the opacity of the entry in our build function, which means we // our state has changed. }); } @override Widget build(BuildContext context) { // These lists are filled backwards. For the offstage children that // does not matter since they aren't rendered, but for the onstage
    // children we reverse the list below before adding it to the tree.
    final List<Widget> onstageChildren = <Widget>[];
    final List<Widget> offstageChildren = <Widget>[];
    bool onstage = true;
    for (int i = _entries.length - 1; i >= 0; i -= 1) {
      final OverlayEntry entry = _entries[i];
      if (onstage) {
        onstageChildren.add(_OverlayEntry(entry));
        if (entry.opaque)
          onstage = false;
      } else if (entry.maintainState) {
        offstageChildren.add(TickerMode(enabled: false, child: _OverlayEntry(entry)));
      }
    }
    return _Theatre(
      onstage: Stack(
        fit: StackFit.expand,
        children: onstageChildren.reversed.toList(growable: false),
      ),
      offstage: offstageChildren,
    );
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    // TODO(jacobr): use IterableProperty instead as that would
    // provide a slightly more consistent string summary of the List.
    properties.add(DiagnosticsProperty<List<OverlayEntry>>('entries', _entries));
  }
}

複製代碼
相關文章
相關標籤/搜索