Flutter 編輯選擇框(TextFiledSelect)

語言: CN / TW / HK

Flutter 編輯選擇框(TextFiledSelect)

先看效果:

效果演示.gif

吶,就上圖實現的功能(可能官方有,可我沒找到;可能外掛有,可我沒找到)。

憂愁.gif

不說廢話了,直接開整。

  • 思路分析:

    1. 其實懸浮元件大家應該都知道,Flutter中的全域性懸浮用的Overlayer

    2. 那這個元件的重要部分就是讓已經懸浮的選項列表跟隨滾動,這裡用到的是CompositedTransformTarget以及與它配對的跟隨元件CompositedTransformFollower

    3. 其實官方的TextFieldSelectionToolbar就已經是一個很好的例子了,來看圖。

      SelectionToolbar.gif

      從上圖就可以看到,SelectionToolbar實際上就是跟隨文字框滾動而滾動的。

      那麼,具體的實現呢?接著往下看。

tips: 正片從第4步開始,可以直接略過1、2、3步。

1. TextField 中的 SelectionToolbar 的拆解

  • CompositedTransformTargetCompositedTransformFollower

    這兩個元件才是 !關鍵!

    對於它們倆,在官方docs中的解釋:

    CompositedTransformTarget

    CompositedTransformFollower

    好吧(反正我一看文件就頭疼)。簡單來說就是:CompositedTransformTarget 作為目標,CompositedTransformFollower作為跟隨,通過一個 LayerLink 來連結,當CompositedTransformTarget發生變換(偏移)時,CompositedTransformFollower將自動跟隨變換(偏移)。

    我們可以直接定位到官方 TextField 下的相關程式碼。

    TextField -> EditableText -> EditableTextState

    // 虛擬碼,在 Flutter 3.0 中, EditableTextState下的第3282行。 CompositedTransformTarget(    link: _toolbarLayerLink,    child: ... )

    可以看到,這裡是用上了CompositedTransformTarget,並且給了一個_toolbarLayerLink作為link連結。

    我們可以搜尋以下_toolbarLayerLink物件例項。

    _toolbarLayerLink.png

    這裡可以看到,有一個叫做 TextSelectionOverlay的類將它作為引數引用了它,那麼繼續跟下去。

    toolbarLayerLink.png

    到這裡,~~這一個類SelectionOverlay應該就是我們要找的SelectionToolbar了。~~ 繼續跟進去。

    _SelectionToolbarOverlay.png

    繼續跟進之後,我們可以看到,還有一個_SelectionToolbarOverlay(屬實沒想到套得這麼深,那上面加上刪除線),繼續看看它下面的程式碼。

    _SelectionToolbarOverlayState_____build.png

    山路十八彎,不過總算是找到。到此為止,這倆兄弟(CompositedTransformTargetCompositedTransformFollower)就都出現了。

    widget.selectionControls!.buildToolbar(...)

    應該就是構建佈局了,這裡就不進去看了,需要特別注意的是 上圖的 offesteditingRegion下面顯示位置的時候會提到。

2. TextField 中的 SelectionToolbar 的顯示

那接下來就看看,如何讓SelectionToolbar展示出來的)。

SelectionOverlay.png

上圖就是SelectionOverlay的方法大綱,通過畫框的這一些方法命名,應該就能夠知道了它們分別是幹什麼的。

emm,我這裡只是想要知道SelectionToolbar是怎麼出來的,那對於_handles選擇手柄這一塊我就不去看了。那我點開showToolbar()方法,看一下它下面的程式碼。

// 在 Flutter 3.0 中, SelectionOverlay下的第845行。 void showToolbar() {    if (_toolbar != null) {      return;   }    // 這裡構建 OverlayEntry 檢視    _toolbar = OverlayEntry(builder: _buildToolbar);    // 這裡將_toolbar插入螢幕    Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)!.insert(_toolbar!); }

其實到了這一步,SelectionToolbar 的顯示程式碼已經出來了。那如何顯示的,大概就是當文字框被長按,然後呼叫showToolbar()方法,我們再看看隱藏hideToolbar()的實現。

// 在 Flutter 3.0 中, SelectionOverlay下的第899行。 void hideToolbar() {    if (_toolbar == null)      return;    // 這裡將 OverlayEntry 移出螢幕    _toolbar?.remove();    // 然後置空    _toolbar = null; }

那我們可以繼續搜尋定位,showToolbar()hideToolbar()具體是在哪裡被呼叫的。

show和hide的呼叫時機.png

那通過方法名,就應該知道這是手勢識別器,當然這裡的editableText.showToolbar()editableText.hideToolbar()並不是直接呼叫上述的程式碼,而是有各種邏輯巢狀,如果按照文章一步一步跟到這裡了,就能夠看到其中的具體實現,這裡就不說了(哈哈哈,這幾個字出現過好幾次了,不會打我吧 \dog)。

似乎,到這裡,好像大概流程已經差不多了……

!不對,還差點什麼。

3. TextField 中的 SelectionToolbar的顯示位置

我們來看,

SelectionToolbar.gif

上圖,SelectionToolbar顯示定位的是文字框的上方,然後通過CompositedTransformTargetCompositedTransformFollower的繫結實現滾動跟隨,那麼,具體位置是怎麼來的呢?用過OverlayEntry的朋友應該都知道,當OverlayEntry插入螢幕之後,顯示的位置是在螢幕的左上角。而這裡則是顯示在文字框的左上方。

那,這裡應該才是最重要的咯。

不知道各位對上面有沒有showToolbar()方法中的程式碼還有沒有印象,

showToolbar()中有一個,

_toolbar = OverlayEntry(builder: _buildToolbar);

這裡的_buildToolbar就是構建懸浮佈局的方法。(很快啊!我直接貼就上來了。)

// 虛擬碼,在 Flutter 3.0 中, SelectionOverlay下的第961行。 ​ Widget _buildToolbar(BuildContext context) {    if (selectionControls == null)      return Container(); ​    // 取到當前操作物件    final RenderBox renderBox = this.context.findRenderObject()! as RenderBox; ​    // 取到當前操作物件在螢幕上的座標    final Rect editingRegion = Rect.fromPoints(      renderBox.localToGlobal(Offset.zero),      renderBox.localToGlobal(renderBox.size.bottomRight(Offset.zero)),   );       ... // 省略          return _SelectionToolbarOverlay(       ...        layerLink: toolbarLayerLink, // LayerLink        editingRegion: editingRegion, // 傳入座標       ...     ); } }

嗦嘎,這樣不就有座標了!_SelectionToolbarOverlay具體的構建步驟(上面第1步已經提到了,這裡就不再贅述)。

4. (正片)Select 懸浮框的實現。

上面已經提到CompositedTransformTargetCompositedTransformFollower的偏移關係。那麼開始,建資料夾。

做不了.webp

// ---------- 虛擬碼,請勿直接cv ----------------- ​ // _onTap 監聽編輯框點選 void _onTap() {   ... } ​ // _onChanged 監聽編輯框內容的改變 void _onChanged(String text) {   ... } ​ // build Widget build(BuildContext context) {    return CompositedTransformTarget(        link: _selectViewLayerLink,        child: TextField(            onTap: _onTap,            onChanged: _onChanged,       ),   ); } ​

TextField出現上述操作事件時,則應該展示選擇列表。

這裡,選擇列表的佈局就應該如下:

// ---------- 虛擬碼,請勿直接cv ----------------- ​ final RenderBox renderBox = context.findRenderObject()! as RenderBox; // 取到當前物件 _overlayEntry = OverlayEntry(    opaque: false,    builder: (BuildContext context) {        return Stack(            children: [                CompositedTransformFollower(                    link: _selectViewLayerLink,                    showWhenUnlinked: false,                    offset: Offset(0, renderBox.size.height), // 顯示在文字框的正下方。                    child: FadeTransition(                        opacity: _animationController.view,                        child: Material(                            child: Text("我作為下拉框"),                       ),                   ),               ),           ],       );   }, );

效果圖:

最終效果.gif

具體邏輯已經成型,這裡就不在繼續往下了,最終成品待會上傳GitHub。