likes
comments
collection
share

Flutter FocusNode 焦点那点事-(一)

作者站长头像
站长
· 阅读数 47

很多时候, 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();, 调用一下, 软键盘就下去了

Flutter FocusNode 焦点那点事-(一)

这东西里面基本都是私有方法, 能调用的并不多

FocusHighlightMode 这东西是焦点的"模式", 对应触摸和鼠标键盘, 个人认为一般情况下用不到, 移动端就 touch 就可以了

Focus

这东西一般情况下很少能用到, SDK 里有一些地方会用到, Focus 对象本身内部会维护一个 FocusNode, 比如按钮能响应键盘回车之类的焦点就是因为内部有这东西

这个类在 flutter 项目中使用率不算高, 但都是关键处

Flutter FocusNode 焦点那点事-(一) Flutter FocusNode 焦点那点事-(一)

_FocusableActionDetectorState: 对应 FocusableActionDetector 的状态, 这个类被用于 CheckBox, Radio, Switch

FocusScope

这东西很少见有文档讲, 这里我简单的解析一下, 这个也可以说是后面使用的重点, 我在实际开发中遇到有输入框的情况下, 这个控件是我的首选

简单来说, 就是在这东西子控件内的 FocusNode 都会被统一维护

Flutter FocusNode 焦点那点事-(一)

这东西构造方法可以传一些参数, 常用的无非就是 node, canRequestFocus, 之类的.

这里有一个 skipTraversal, 这个参数后面结合例子来看才能说明白

FocusScopeNode

一般和FocusScope成对使用

写代码

入门级写法

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

比如有一个这样的场景

Flutter FocusNode 焦点那点事-(一)

用 app 来说, 就是 4 个输入框, 一个个的点击自然可以, 但是如果要用户体验好是不是应该可以回车一直下一步, 然后最后一条直接提交呢?

模拟一下这个东西很多人的写法

Flutter FocusNode 焦点那点事-(一)

嗯, 点评一下, 嗯 很整齐, 那么... 当你有 10 个的时候怎么办呢? 想想就很美

我们改写下,也许可以这样?

Flutter FocusNode 焦点那点事-(一)

好的, 算你基础扎实, 这样写自然是可以的.

进阶

上面的写法很 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 的样子:

Flutter FocusNode 焦点那点事-(一)

这是因为 TextFieldEditableText 的封装

Flutter FocusNode 焦点那点事-(一) Flutter FocusNode 焦点那点事-(一) Flutter FocusNode 焦点那点事-(一)

然后是在 EditableText 里, attach 到了 context 上

Flutter FocusNode 焦点那点事-(一)

看到这里, 是不是发现其实有的东西很简单, 接下来复杂一下

再进阶

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();
  }
}

这种偶尔旁边多了一个按钮的, 属于比较常见的方式, 然后上面代码突然就不好用了

Flutter FocusNode 焦点那点事-(一)

这时候就需要改代码了

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 就可以了

Flutter FocusNode 焦点那点事-(一)

完整代码:

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方法

后记

本篇到此, 本系列的后续预计要深爬一下源码

以上

转载自:https://juejin.cn/post/6854573216015499271
评论
请登录