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

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

原文做者:medium.com/@perclassongit

發佈時間:2018年9月2日github

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

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

你想想,爲了達到這個目的,你必須把整個屏幕從新設計成一個Stack,而後計算出每一個widget必須顯示的確切位置。這很是麻煩,很是嚴格,很是容易出錯,並且感受就是不對。但還有另外一種方法。函數

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

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

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

什麼是Overlay widget?

官方文檔對Overlay widget的定義是。this

能夠獨立管理的條目。

疊加讓獨立的子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一個陰影,使它看起來好像真的是浮動的。

就是這樣! 咱們的建議框如今漂浮在全部其餘東西的上方了

獎勵:跟着卷軸走!

在咱們離開以前,讓咱們試着再學習一件事! 若是咱們的視圖是能夠滾動的,那麼咱們可能會注意到一些東西。

建議框會跟着咱們滾動!

建議框會粘在屏幕上的位置上。在某些狀況下,這多是咱們想要的,但在這種狀況下,咱們不但願這樣,咱們但願它跟隨咱們的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(免費版)翻譯

相關文章
相關標籤/搜索