Flutter FocusNode 焦點那點事-(一)

CaiJingLong發表於2020-07-23

很多時候, flutter 中需要處理輸入的焦點, 我們們今天就來看看控制元件怎麼用

本篇可以視為簡單使用, 而不會深入原始碼去探討怎麼附著, 主要是 Focus 系列控制元件的使用, 和怎麼在多輸入框之間反覆橫跳

環境說明

  1. 本篇基本基於 flutter sdk 的 1.17.5 版本來看, 其他版本應該大同小異, 但很多東西可能會隨時間變化, 未來是否有效請繼續驗證
  2. 本篇基本是針對移動端來說的
  3. 寫本文時, flutter web 的焦點比較迷, 似乎和移動版不太一樣, 所以暫時略過不表
  4. desktop 版只嘗試了 macOS, 其他的桌面引擎請自行校驗對錯

相關 dart class

flutter 中, 和焦點相關聯類有如下幾個:

  • FocusNode: 這個可以說是最常用到的, 核心類之一
  • FocusManager: 單例類, 整個 flutter 應用的焦點管理核心都是這東西在處理, 包括和原生互動彈出軟鍵盤之類的操作
  • Focus: 一個 Widget, 用於給控制元件"新增"焦點能力, 包起來就行, InkWell 之類的控制元件能獲取焦點能力都是靠這東西
  • FocusScope: 一個 Widget, Focus 的子類, 被這東西包起來的所有的子 widget 的 FocusNode 都會被自動註冊到這個裡面, 接受統一管理
  • FocusScopeNode: 這東西本身是 FocusNode 的子類, 但是它主要是給 FocusScope 用的,擴充套件了 FocusNode 的行為
  • FocusTraversalPolicy, FocusTraversalGroup: 這兩個東西是 focus node 的策略, 用於排序哪個是下一個焦點的問題, 這兩個東西本篇應該不講, 有興趣的可以去看官方文件, 目前個人認為應該用不上

FocusNode

這東西講的人很多, 我也就不展開了, 簡單的說一下幾個方法

  • canRequestFocus: 是否能請求焦點
  • context: 焦點"附著"的 widget 的 BuildContext
  • hasFocus: 是否有焦點
  • unfocus: 放棄焦點, 如果當前 node 有焦點,並呼叫這個, 就放棄了焦點, 如果同時有軟鍵盤彈起, 則軟鍵盤收起
  • requestFocus: 請求焦點, 這個方法呼叫後, 會把焦點移到當前

備註: 有很多其他的方法, 對於普通朋友和正常的應用場景很難用到, 作為程式框架有提供, 但是個人觀點不必一定要了解, 只要知道主要方法即可

FocusManager

這東西是一個單例的,通過FocusManager.instance獲取

有一個常用方法瞭解一下: FocusManager.instance.primaryFocus.unfocus();, 呼叫一下, 軟鍵盤就下去了

1595307999

這東西里面基本都是私有方法, 能呼叫的並不多

FocusHighlightMode 這東西是焦點的"模式", 對應觸控和滑鼠鍵盤, 個人認為一般情況下用不到, 移動端就 touch 就可以了

Focus

這東西一般情況下很少能用到, SDK 裡有一些地方會用到, Focus 物件本身內部會維護一個 FocusNode, 比如按鈕能響應鍵盤迴車之類的焦點就是因為內部有這東西

這個類在 flutter 專案中使用率不算高, 但都是關鍵處

1595308605
1595311067

_FocusableActionDetectorState: 對應 FocusableActionDetector 的狀態, 這個類被用於 CheckBox, Radio, Switch

FocusScope

這東西很少見有文件講, 這裡我簡單的解析一下, 這個也可以說是後面使用的重點, 我在實際開發中遇到有輸入框的情況下, 這個控制元件是我的首選

簡單來說, 就是在這東西子控制元件內的 FocusNode 都會被統一維護

1595316377

這東西構造方法可以傳一些引數, 常用的無非就是 node, canRequestFocus, 之類的.

這裡有一個 skipTraversal, 這個引數後面結合例子來看才能說明白

FocusScopeNode

一般和FocusScope成對使用

寫程式碼

入門級寫法

嗯, 前面都是概念性的東西, 很多朋友都不想看, 而且也沒啥意思

比如有一個這樣的場景

1595317002

用 app 來說, 就是 4 個輸入框, 一個個的點選自然可以, 但是如果要使用者體驗好是不是應該可以回車一直下一步, 然後最後一條直接提交呢?

模擬一下這個東西很多人的寫法

1595317293

嗯, 點評一下, 嗯 很整齊, 那麼... 當你有 10 個的時候怎麼辦呢? 想想就很美

我們改寫下,也許可以這樣?

1595317499

好的, 算你基礎紮實, 這樣寫自然是可以的.

進階

上面的寫法很 dart, 但是不 flutter, 我們 flutter 的寫法可以改成這樣

import 'package:flutter/material.dart';

class Example3 extends StatefulWidget {
  @override
  _Example3State createState() => _Example3State();
}

class _Example3State extends State<Example3> {
  FocusScopeNode node = FocusScopeNode();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: FocusScope(
        node: node,
        child: SingleChildScrollView(
          child: Column(
            children: <Widget>[
              for (var i = 0; i < 10; i++) buildTextField(),
            ],
          ),
        ),
      ),
    );
  }

  TextField buildTextField() {
    return TextField(
      onEditingComplete: () {
        if (node.focusedChild == node.children.last) {
          print('submit');
        } else {
          node.nextFocus();
        }
      },
    );
  }
}

複製程式碼

這次連 FocusNode 都不需要自己寫了, 直接用 Scope 裡的

這個 example 的樣子:

1595319363

這是因為 TextFieldEditableText 的封裝

1595318868
1595318898
1595318882

然後是在 EditableText 裡, attach 到了 context 上

1595319082

看到這裡, 是不是發現其實有的東西很簡單, 接下來複雜一下

再進階

import 'package:flutter/material.dart';

class Example3 extends StatefulWidget {
  @override
  _Example3State createState() => _Example3State();
}

class _Example3State extends State<Example3> {
  FocusScopeNode node = FocusScopeNode();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: FocusScope(
        node: node,
        child: SingleChildScrollView(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              children: <Widget>[
                for (var i = 0; i < 5; i++) buildTextField(),
                Row(
                  children: <Widget>[
                    Expanded(
                      child: TextField(
                        onEditingComplete: onEdit,
                      ),
                    ),
                    RaisedButton(
                      onPressed: () {},
                      child: Text('假裝獲取驗證碼'),
                    ),
                  ],
                ),
                for (var i = 0; i < 5; i++) buildTextField(),
              ],
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          print(node.traversalChildren.length);
        },
        child: Icon(Icons.check),
      ),
    );
  }

  TextField buildTextField() {
    return TextField(
      onEditingComplete: onEdit,
    );
  }

  void onEdit() {
    node.nextFocus();
  }
}

複製程式碼

這種偶爾旁邊多了一個按鈕的, 屬於比較常見的方式, 然後上面程式碼突然就不好用了

1595323601

這時候就需要改程式碼了

floatingActionButton: FloatingActionButton(
onPressed: () {
    print(node.children.length); // 12
},
child: Icon(Icons.check),
),
複製程式碼

為啥變 12 了呢, 不是隻有 11 個輸入框嗎?

這裡就和我開始說的對上了, 很多按鈕也有 focus.

那麼怎麼在回車時跳過這個按鈕呢

   RaisedButton(
    onPressed: () {},
    focusNode: FocusNode(skipTraversal: true),
    child: Text('假裝獲取驗證碼'),
),

複製程式碼

是的, 就是這樣, 給按鈕手動傳入一個 FocusNode, 然後 skip 就可以了

1595324883

完整程式碼:

import 'package:flutter/material.dart';

class Example3 extends StatefulWidget {
  @override
  _Example3State createState() => _Example3State();
}

class _Example3State extends State<Example3> {
  FocusScopeNode node = FocusScopeNode();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: FocusScope(
        node: node,
        child: SingleChildScrollView(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              children: <Widget>[
                for (var i = 0; i < 5; i++) buildTextField(),
                Row(
                  children: <Widget>[
                    Expanded(
                      child: TextField(
                        onEditingComplete: onEdit,
                      ),
                    ),
                    RaisedButton(
                      onPressed: () {},
                      focusNode: FocusNode(skipTraversal: true),
                      child: Text('假裝獲取驗證碼'),
                    ),
                  ],
                ),
                for (var i = 0; i < 5; i++) buildTextField(),
              ],
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          print(node.traversalChildren.length);
        },
        child: Icon(Icons.check),
      ),
    );
  }

  TextField buildTextField() {
    return TextField(
      onEditingComplete: onEdit,
    );
  }

  void onEdit() {
    node.nextFocus();
  }
}

複製程式碼

所以總結一下步驟

  1. 將所有的輸入框包在一個 FocusScope 裡, 設定 FocusScopeNode.
  2. 將有焦點但不是輸入框的控制元件設定一個 FocusNode(skipTraversal: true)
  3. 使用FocusScopeNodenextFocus方法

後記

本篇到此, 本系列的後續預計要深爬一下原始碼

以上

相關文章