摘要

#

選單(context menus),或稱文字選取工具列(text selection toolbars),是在 Flutter 中長按或右鍵點擊文字時出現的選單,通常會顯示 剪下複製貼上 以及 全選 等選項。過去,只能透過ToolbarOptionsTextSelectionControls 來有限度地自訂這些選單。現在,這些選單已經像 Flutter 其他功能一樣,能夠以元件(Widget)組合方式自訂,原本的特定設定參數也已被棄用。

背景

#

過去,可以利用 TextSelectionControls 來停用選單中的按鈕,但若要進一步自訂,則必須複製並修改框架中數百行的自訂類別。現在,這一切都被一個簡單的建構器函式 contextMenuBuilder 取代,讓你可以將任何 Flutter 元件(Widget)用作選單。

變更說明

#

選單現在是透過 contextMenuBuilder 參數來建構,該參數已加入所有文字編輯與文字選取元件(Widget)。如果未提供該參數,Flutter 會自動設為預設值,並根據不同平台建立正確的選單。所有這些預設元件(Widget)都已公開給使用者重複利用。自訂選單現在只需使用 contextMenuBuilder 回傳你想要的任何元件(Widget),也可以重複利用內建的選單元件。

以下範例展示如何在選取電子郵件地址時,於預設選單中新增 傳送電子郵件 按鈕。完整程式碼可在 GitHub 的 samples repository 中的 email_button_page.dart 找到。

dart
TextField(
  contextMenuBuilder: (context, editableTextState) {
    final TextEditingValue value = editableTextState.textEditingValue;
    final List<ContextMenuButtonItem> buttonItems =
        editableTextState.contextMenuButtonItems;
    if (isValidEmail(value.selection.textInside(value.text))) {
      buttonItems.insert(
          0,
          ContextMenuButtonItem(
            label: 'Send email',
            onPressed: () {
              ContextMenuController.removeAny();
              Navigator.of(context).push(_showDialog(context));
            },
          ));
    }
    return AdaptiveTextSelectionToolbar.buttonItems(
      anchors: editableTextState.contextMenuAnchors,
      buttonItems: buttonItems,
    );
  },
)

各種不同自訂右鍵選單(context menu)的範例,可在 GitHub 的 samples repo 中找到。

所有相關的已棄用功能,皆已標註棄用警告:「請改用 contextMenuBuilder。」

遷移指南

#

一般來說,所有先前已被棄用的右鍵選單(context menu)相關變更,現在都需要在相關的文字編輯或文字選取元件(Widget)上,使用 contextMenuBuilder 參數( 例如TextField)。若要使用 Flutter 內建的右鍵選單,請回傳像 AdaptiveTextSelectionToolbar 這樣的內建右鍵選單元件;若需要完全自訂,則可回傳你自訂的元件。

為了遷移至 contextMenuBuilder,下列參數與類別已被棄用。

這個類別先前用於明確啟用或停用右鍵選單中的特定按鈕。在這項變更之前,你可能會像這樣將它傳入 TextField 或其他元件:

dart
// Deprecated.
TextField(
  toolbarOptions: ToolbarOptions(
    copy: true,
  ),
)

現在,你可以透過調整傳遞給AdaptiveTextSelectionToolbarbuttonItems來達到相同的效果。例如,你可以確保 Cut 按鈕永遠不會出現,但其他按鈕則會如常顯示:

dart
TextField(
  contextMenuBuilder: (context, editableTextState) {
    final List<ContextMenuButtonItem> buttonItems =
        editableTextState.contextMenuButtonItems;
    buttonItems.removeWhere((ContextMenuButtonItem buttonItem) {
      return buttonItem.type == ContextMenuButtonType.cut;
    });
    return AdaptiveTextSelectionToolbar.buttonItems(
      anchors: editableTextState.contextMenuAnchors,
      buttonItems: buttonItems,
    );
  },
)

或者,你也可以確保 Cut 按鈕始終且僅顯示:

dart
TextField(
  contextMenuBuilder: (context, editableTextState) {
    return AdaptiveTextSelectionToolbar.buttonItems(
      anchors: editableTextState.contextMenuAnchors,
      buttonItems: <ContextMenuButtonItem>[
        ContextMenuButtonItem(
          onPressed: () {
            editableTextState.cutSelection(SelectionChangedCause.toolbar);
          },
          type: ContextMenuButtonType.cut,
        ),
      ],
    );
  },
)

TextSelectionControls.canCut 以及其他按鈕布林值

#

這些布林值先前與ToolbarOptions.cut等具有相同的效果,用於啟用或停用特定按鈕。在此變更之前,你可能會透過覆寫TextSelectionControls並設定這些布林值,來隱藏或顯示按鈕,例如:

dart
// Deprecated.
class _MyMaterialTextSelectionControls extends MaterialTextSelectionControls {
  @override
  bool canCut() => false,
}

請參閱前一節關於ToolbarOptions的說明,以了解如何使用contextMenuBuilder達到類似效果。

TextSelectionControls.handleCut 以及其他按鈕回呼函式

#

這些函式允許在按下按鈕時修改所呼叫的回呼函式。在這項變更之前,你可能會透過覆寫這些處理程序方法來修改右鍵選單(context menu)按鈕的回呼函式,例如:

dart
// Deprecated.
class _MyMaterialTextSelectionControls extends MaterialTextSelectionControls {
  @override
  bool handleCut() {
    // My custom cut implementation here.
  },
}

這仍然可以透過 contextMenuBuilder 實現,包括在自訂處理程序中呼叫原本按鈕的動作,以及使用像 AdaptiveTextSelectionToolbar.buttonItems 這樣的工具列元件 (Widgets)。

以下範例展示如何修改 Copy 按鈕,讓它在執行原本的複製邏輯之外,額外顯示一個對話框。

dart
TextField(
  contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
    final List<ContextMenuButtonItem> buttonItems =
        editableTextState.contextMenuButtonItems;
    final int copyButtonIndex = buttonItems.indexWhere(
      (ContextMenuButtonItem buttonItem) {
        return buttonItem.type == ContextMenuButtonType.copy;
      },
    );
    if (copyButtonIndex >= 0) {
      final ContextMenuButtonItem copyButtonItem =
          buttonItems[copyButtonIndex];
      buttonItems[copyButtonIndex] = copyButtonItem.copyWith(
        onPressed: () {
          copyButtonItem.onPressed();
          Navigator.of(context).push(
            DialogRoute<void>(
              context: context,
              builder: (BuildContext context) =>
                const AlertDialog(
                  title: Text('Copied, but also showed this dialog.'),
                ),
            );
          )
        },
      );
    }
    return AdaptiveTextSelectionToolbar.buttonItems(
      anchors: editableTextState.contextMenuAnchors,
      buttonItems: buttonItems,
    );
  },
)

一個完整的修改內建 context menu(右鍵選單)操作的範例,可以在 samples 儲存庫的 modified_action_page.dart (GitHub 上)找到。

這個函式產生 context menu 元件(Widget)的方式與 contextMenuBuilder 類似,但需要更多的設定才能使用。在這個變更之前,你可能會像這樣,將 buildToolbar 覆寫為 TextSelectionControls 的一部分:

dart
// Deprecated.
class _MyMaterialTextSelectionControls extends MaterialTextSelectionControls {
  @override
  Widget buildToolbar(
    BuildContext context,
    Rect globalEditableRegion,
    double textLineHeight,
    Offset selectionMidpoint,
    List<TextSelectionPoint> endpoints,
    TextSelectionDelegate delegate,
    ClipboardStatusNotifier clipboardStatus,
    Offset lastSecondaryTapDownPosition,
  ) {
    return _MyCustomToolbar();
  },
}

現在你可以直接將 contextMenuBuilder 作為參數傳遞給 TextField(以及其他相關函式)。在傳遞給 buildToolbar 的參數中所提供的資訊,現在可以從傳遞給 contextMenuBuilderEditableTextState 中取得。

以下範例展示如何從零開始建立一個完全自訂的工具列(toolbar),同時仍然使用預設按鈕。

dart
class _MyContextMenu extends StatelessWidget {
  const _MyContextMenu({
    required this.anchor,
    required this.children,
  });

  final Offset anchor;
  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          top: anchor.dy,
          left: anchor.dx,
          child: Container(
            width: 200,
            height: 200,
            color: Colors.amberAccent,
            child: Column(
              children: children,
            ),
          ),
        ),
      ],
    );
  }
}

class _MyTextField extends StatelessWidget {
  const _MyTextField();

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
      maxLines: 4,
      minLines: 2,
      contextMenuBuilder: (context, editableTextState) {
        return _MyContextMenu(
          anchor: editableTextState.contextMenuAnchors.primaryAnchor,
          children: AdaptiveTextSelectionToolbar.getAdaptiveButtons(
            context,
            editableTextState.contextMenuButtonItems,
          ).toList(),
        );
      },
    );
  }
}

在範例程式庫中,於 GitHub 的 custom_menu_page.dart 可以找到建立自訂右鍵選單(context menu)的完整範例。

時程

#

合併於版本:3.6.0-0.0.pre
穩定版釋出:3.7.0

參考資料

#

API 文件:

相關議題(issues):

相關 PR: