[Flutter翻譯]Flutter: 使用Overlay顯示浮動widget

Sunbreak發表於2020-07-07

原文地址:medium.com/flutter/imp…

原文作者:medium.com/@perclasson

釋出時間:2018年9月2日

想象一下:你設計了你的迷人的表格。

[Flutter翻譯]Flutter: 使用Overlay顯示浮動widget

你把它發給你的產品經理,他看了看說:"那我得把整個國家的名字都打進去?你就不能在我輸入的時候給我看看建議嗎?"然後你就想:"嗯,他說得對!"嗯,他是對的!" 所以你決定實現一個 "typeahead",一個 "自動完成 "或任何你想叫它的東西。一個文字欄位,在使用者輸入時顯示建議。你開始工作......你知道如何獲得建議,你知道如何做邏輯,你什麼都知道......除了如何讓建議漂浮在其他widget之上。

你想一想,為了達到這個目的,你必須把整個螢幕重新設計成一個Stack,然後計算出每個widget必須顯示的確切位置。這非常麻煩,非常嚴格,非常容易出錯,而且感覺就是不對。但還有另一種方法。

你可以使用Flutter預先提供的Stack,即 Overlay

在這篇文章中,我將解釋如何使用Overlaywidget來建立浮在其他一切之上的widget,而不必重組你的整個檢視。

你可以用它來建立自動完成建議,工具提示,或者基本上任何浮動的東西。

什麼是Overlay widget?

官方文件對Overlay widget的定義是。

可以獨立管理的條目。

疊加讓獨立的子widget通過插入到疊加的堆疊中,將視覺元素 "漂浮 "在其他widget之上。

這正是我們要找的。當我們建立MaterialApp時,它會自動建立一個Navigator,而Navigator又會建立一個Overlay;一個Stack widget,Navigator用它來管理檢視的顯示。

所以我們來看看如何使用Overlay來解決我們的問題。

注意:本文關注的是顯示浮動widget,因此不會過多地介紹實現typeahead(自動完成)欄位的細節。如果你對一個編碼良好、高度可定製的typeahead widget感興趣,一定要看看我的包,flutter_typeahead

初始程式

讓我們從簡單的形式開始。

Scaffold(
  body: Padding(
    padding: const EdgeInsets.all(50.0),
    child: Form(
      child: ListView(
        children: <Widget>[
          TextFormField(
            decoration: InputDecoration(
                labelText: 'Address'
            ),
          ),
          SizedBox(height: 16.0,),
          TextFormField(
            decoration: InputDecoration(
                labelText: 'City'
            ),
          ),
          SizedBox(height: 16.0,),
          TextFormField(
            decoration: InputDecoration(
                labelText: 'Address'
            ),
          ),
          SizedBox(height: 16.0,),
          RaisedButton(
            child: Text('SUBMIT'),
            onPressed: () {
              // submit the form
            },
          )
        ],
      ),
    ),
  ),
)
複製程式碼
  • 它是一個簡單的檢視,包含三個文字欄位:國家、城市和地址。

然後,我們將國家欄位抽象成自己的有狀態widget,我們稱之為 CountriesField

class CountriesField extends StatefulWidget {
  @override
  _CountriesFieldState createState() => _CountriesFieldState();
}

class _CountriesFieldState extends State<CountriesField> {

  
  @override
  Widget build(BuildContext context) {
    return TextFormField(
      decoration: InputDecoration(
        labelText: 'Country'
      ),
    );
  }
}
複製程式碼

接下來我們要做的是,每當欄位接收到焦點時就顯示一個浮動列表,每當焦點丟失時就隱藏該列表。你可以根據你的用例來改變這個邏輯。你可能想只在使用者輸入一些字元時才顯示它,而在使用者點選Enter時刪除它。在所有情況下,讓我們來看看如何顯示這個浮動widget。

class CountriesField extends StatefulWidget {
  @override
  _CountriesFieldState createState() => _CountriesFieldState();
}

class _CountriesFieldState extends State<CountriesField> {

  final FocusNode _focusNode = FocusNode();

  OverlayEntry _overlayEntry;

  @override
  void initState() {
    _focusNode.addListener(() {
      if (_focusNode.hasFocus) {

        this._overlayEntry = this._createOverlayEntry();
        Overlay.of(context).insert(this._overlayEntry);

      } else {
        this._overlayEntry.remove();
      }
    });
  }

  OverlayEntry _createOverlayEntry() {

    RenderBox renderBox = context.findRenderObject();
    var size = renderBox.size;
    var offset = renderBox.localToGlobal(Offset.zero);

    return OverlayEntry(
      builder: (context) => Positioned(
        left: offset.dx,
        top: offset.dy + size.height + 5.0,
        width: size.width,
        child: Material(
          elevation: 4.0,
          child: ListView(
            padding: EdgeInsets.zero,
            shrinkWrap: true,
            children: <Widget>[
              ListTile(
                title: Text('Syria'),
              ),
              ListTile(
                title: Text('Lebanon'),
              )
            ],
          ),
        ),
      )
    );
  }

  @override
  Widget build(BuildContext context) {
    return TextFormField(
        focusNode: this._focusNode,
      decoration: InputDecoration(
        labelText: 'Country'
      ),
    );
  }
}
複製程式碼
  • 我們為TextFormField分配一個FocusNode,並在initState中為其新增一個監聽器。我們將使用這個監聽器來檢測欄位何時獲得/失去焦點。

  • 每當我們接收到焦點 (_focusNode.hasFocus == true),我們就使用 _createOverlayEntry 建立一個 OverlayEntry,並使用 Overlay.of(context).insert 將它插入到最近的 Overlay widget 中。

  • 每當我們失去焦點 (_focusNode.hasFocus == false),我們就會使用 _overlayEntry.remove 刪除我們新增的覆蓋條目。

  • _createOverlayEntry使用context.findRenderObject函式,查詢我們widget的渲染框。這個渲染框使我們能夠知道widget的位置、大小和其他渲染資訊。這將幫助我們以後知道在哪裡放置我們的浮動列表。

  • _createOverlayEntry使用渲染框來獲取widget的大小,它還使用renderBox.localToGlobal來獲取widget在螢幕中的座標。我們為localToGlobal方法提供了Offset.zero,這意味著我們要在這個渲染框裡面獲取 (0,0) 座標,並將其轉換為螢幕上的對應座標。

  • 然後我們建立一個OverlayEntry,這是一個用於顯示Overlay中的widget的widget。

  • OverlayEntry的內容是一個Positioned widget。請記住,Positioned widgets只能插入Stack中,但也請記住,Overlay確實是一個Stack

  • 我們設定Positioned widget的座標,我們給它與TextField相同的x座標,相同的寬度,相同的y座標,但為了不覆蓋TextField,我們將其向底部移動一點。

  • Positioned裡面,我們顯示一個ListView,裡面有我們想要的建議(我在例子中硬編碼了幾個條目)。請注意,我把所有的東西都放在一個Material widget裡面。這有兩個原因:因為Overlay預設不包含Material widget,而許多widget如果沒有Material祖先就無法顯示,而且Material widget提供了仰角屬性,允許我們給widget一個陰影,使它看起來好像真的是浮動的。

就是這樣! 我們的建議框現在漂浮在所有其他東西的上方了

獎勵:跟著卷軸走!

在我們離開之前,讓我們試著再學習一件事! 如果我們的檢視是可以滾動的,那麼我們可能會注意到一些東西。

[Flutter翻譯]Flutter: 使用Overlay顯示浮動widget

建議框會跟著我們滾動!

建議框會粘在螢幕上的位置上。在某些情況下,這可能是我們想要的,但在這種情況下,我們不希望這樣,我們希望它跟隨我們的TextField

這裡的關鍵是 "跟隨 "這個詞。Flutter為我們提供了兩個widget:CompositedTransformFollowerCompositedTransformTarget。簡單的說,如果我們把一個跟隨者和一個目標連結起來,那麼跟隨者就會跟隨目標,無論它走到哪裡! 要連結一個跟隨者和一個目標,我們必須為它們提供相同的LayerLink

因此,我們將用CompositedTransformFollower包裝我們的建議框,用CompositedTransformTarget包裝我們的TextField。然後,我們將通過為它們提供相同的LayerLink來連結它們。這將使建議框跟隨TextField走到哪裡,就跟到哪裡。

class CountriesField extends StatefulWidget {
  @override
  _CountriesFieldState createState() => _CountriesFieldState();
}

class _CountriesFieldState extends State<CountriesField> {

  final FocusNode _focusNode = FocusNode();

  OverlayEntry _overlayEntry;

  final LayerLink _layerLink = LayerLink();

  @override
  void initState() {
    _focusNode.addListener(() {
      if (_focusNode.hasFocus) {

        this._overlayEntry = this._createOverlayEntry();
        Overlay.of(context).insert(this._overlayEntry);

      } else {
        this._overlayEntry.remove();
      }
    });
  }

  OverlayEntry _createOverlayEntry() {

    RenderBox renderBox = context.findRenderObject();
    var size = renderBox.size;

    return OverlayEntry(
      builder: (context) => Positioned(
        width: size.width,
        child: CompositedTransformFollower(
          link: this._layerLink,
          showWhenUnlinked: false,
          offset: Offset(0.0, size.height + 5.0),
          child: Material(
            elevation: 4.0,
            child: ListView(
              padding: EdgeInsets.zero,
              shrinkWrap: true,
              children: <Widget>[
                ListTile(
                  title: Text('Syria'),
                  onTap: () {
                    print('Syria Tapped');
                  },
                ),
                ListTile(
                  title: Text('Lebanon'),
                  onTap: () {
                    print('Lebanon Tapped');
                  },
                )
              ],
            ),
          ),
        ),
      )
    );
  }

  @override
  Widget build(BuildContext context) {
    return CompositedTransformTarget(
      link: this._layerLink,
      child: TextFormField(
          focusNode: this._focusNode,
        decoration: InputDecoration(
          labelText: 'Country'
        ),
      ),
    );
  }
}
複製程式碼
  • 我們在OverlayEntry中用CompositedTransformFollower包裝了我們的Material widget,用CompositedTransformTarget包裝了TextFormField

  • 我們為跟隨者目標提供了同一個LayerLink例項。這將導致跟隨者目標具有相同的座標空間,使其有效地跟隨它。

  • 我們從 Positioned widget 中刪除了 topleft 屬性。這些屬性不再需要了,因為在預設情況下,跟隨者將擁有與目標相同的座標。然而,我們保留了Positionedwidth屬性,因為如果不對其進行約束,跟隨者往往會無限延伸。

  • 我們為CompositedTransformFollower提供了一個偏移量,以禁止它覆蓋TextField(和之前一樣)。

  • 最後,我們將showWhenUnlinked設定為false,當TextField在螢幕上不可見時(比如當我們滾動到底部太遠時),隱藏OverlayEntry

就這樣,我們的OverlayEntry現在跟隨了我們的TextField!

重要提示CompositedTransformFollower還是有點bug;即使當目標不再可見時,跟隨者從螢幕上隱藏起來,跟隨者還是會響應點選事件。我已經向Flutter團隊開了一個問題。

github.com/flutter/flu…

並將在問題解決後更新帖子。


Overlay是一個強大的widget,它為我們提供了一個方便的Stack來放置我們的浮動widget。我已經成功地使用它來建立flutter_typeahead,我相信你也可以將它用於各種用例。 我希望這對你有用。讓我知道你的想法


通過www.DeepL.com/Translator(免費版)翻譯

相關文章