Flutter-Focus树

Focus(焦点)树

  • 作为android开发者都知道,输入框乃是需要获取焦点才可以输入的。在flutter也不例外,Flutter View是以树的形式来构建的,那么Focus其实也是以树形结构来构建的。其中 FocusScopeNode 是根节点,拥有一组focus的父节点。那么下面就围绕flutter的焦点树来做个简单的介绍。
  • 焦点API介绍
    FocusScopeNode :App的根节点(焦点树的根节点),拥有一组focus的父节点。FocusScopeNode的参数列表中包含:children(包括的全部焦点事件)、focusedChildren(请求焦点的焦点事件)等属性。 FocusScope :作为一个Widget的焦点节点(父节点),管理该Widget下的所有children的焦点节点(子节点)。 FocusScope 属于1中的子焦点。 Focus :创建Widget,作为 FocusScope 的子焦点,提供给自身child焦点。 FocusNode :焦点自身事件,也是输入框组件需要的焦点事件。FocusNode构造函数中有debugLabel等参数,其中可以通过debugLabel在焦点树中找到对应的焦点事件。
    温馨提示 :感兴趣可以去看focus_manager.dart类源码,给点耐心和时间看就可以了。
    //官网demo(解释FocusScope与Focus的用法)
    @override
     Widget build(BuildContext context) {
        //创建FocusScope,自己想管理child之间焦点抢占逻辑,并且依附在根节点上(FocusScopeNode)
       return FocusScope(
         debugLabel: 'Scope',
         autofocus: true,
         child: DefaultTextStyle(
           //提供给child一个焦点事件。
           child: Focus(
             debugLabel: 'Button',
             child: Builder(
               builder: (BuildContext context) {
                //获取附近的Focus的焦点事件
                final FocusNode focusNode = Focus.of(context);
                 final bool hasFocus = focusNode.hasFocus;
                 return GestureDetector(
                   onTap: () {
                     if (hasFocus) {
                        //取消焦点获取
                       focusNode.unfocus();
                     } else {
                        //当前Build Widget获取焦点。
                       focusNode.requestFocus();
                   child: *******,
    //小白用法demo
    ///创建一个焦点事件,最好带上debugLabel,要不然找的时候一堆focus,不知道哪个Focus是哪个页面的,变成野指针类似了。
    final FocusNode _verifyFocus = FocusNode(debugLabel: "verify");
    ///告诉焦点树的根节点,当前的_verifyFocus需要焦点,把焦点夺过来。
    FocusScope.of(focusContext()).requestFocus(_verifyFocus);
    ///当Widget从树从被干掉后,也要记得把focus从焦点树中移除。(后续会补充FocusAttachment与FocusManager的讲解)
    @override
      void dispose() {
        _verifyFocus.dispose();
        super.dispose();
    

    如何拥有自己的FocusScopeNode

  • 在创建FocusScope的时候就指定一个node,后续指定可以用这个FocusScopeNode来添加想要请求焦点的FocusNode。
  • 方法二(麻烦点):

  • 当你想拥有一个自己的FocusScopeNode的话,必须一同使用FocusScope + Builder,Builder组件应该是用来切换上下文,形成一个独立的context。因为在widget的build中的context都是根节点的context,如果你想自己的widget拥有一组独立的FocusScopeNode,就必须配合Builder一同使用。

  • 当你拥有了自己的FocusScopeNode后,该widget所有焦点逻辑都应该有该FocusScopeNode来管理。而不是通过根节点的FocusScopeNode。

  • 3.代码演示如下:

    ///方法一:创建FocusScope时候自定focusScopeNode,后续的FocusNode直接可以用
    final FocusScopeNode _phoneFocusScope = FocusScopeNode(debugLabel: "phone_widgets");
    @override
      Widget build(BuildContext context) {
        return FocusScope(
          autofocus: true,
          node: _phoneFocusScope,
          child:xxxxxxxx
    //请求焦点时候,因为当前widget下的全部焦点在_phoneFocusScope下了,所以直接request就可以了。
    _verifyFocus.requestFocus(); // or _phoneFocusScope.requestFocus(_verifyFocus); 
    ///方法二 : 不指定的话内部会自行生成一个
    FocusScopeNode _phoneFocusScope;
    return FocusScope(
          autofocus: true,
          child: Builder(builder: (BuildContext context){
          //这里假如不加入Builder,直接用FocusScope.of(context)是获取根节点的FocusScopeNode
          _phoneFocusScope = FocusScope.of(context);
          return xxxxx;  
    
    FocusScope.of与Focus.of区别
  • FocusScope.of(context),通过context寻找树中最近的FocusScope。
  • Focus.of(context),通过context寻找树中最近的focus。
  • 如何查找对应的FocusNode。

    所有的焦点的创建都已attach到根节点(FocusScopeNode)上,所以当我们要在某些场景下通过接口回调操作某个焦点事件时,例如TabView的切换我想把上个页面的焦点事件给取消。

    普通FocusNode的形式
  • 通过FocusScope.of(context)拿到整个焦点树的根节点,然后通过children去遍历所需要寻找的焦点。代码示例如下:
  • //这里的num是上个页面的焦点事件,当index == 1的时候就是切到下个页面了。应该把软键盘给收起来。所以要执行unfocus。
     _tabController.addListener(() {
          _autoRequestFocus();
    _autoRequestFocus() {
        FocusScope.of(context).children.where((FocusNode node) => node.debugLabel == "num").forEach((FocusNode node) {
          if (node.debugLabel == "num") {
            if (_tabController.index == 1) {
              //整个组的焦点都放弃掉,源码里面会给child
              node.unfocus();
            } else {
              FocusScope.of(context).requestFocus(node);
          } else {
            node.unfocus();
    
    FocusScope的形式
    //创建FocusScopeNode,自己管理当前widget下xxxx布局的全部焦点
    final FocusScopeNode _phoneFocusScope = FocusScopeNode(debugLabel: "phone_widgets");
    return FocusScope(
          autofocus: true,
          node: _phoneFocusScope,
          child: xxxxx
    //当widget不可见后,把抢占焦点行为给放弃
    _tabController.addListener(() {
          _autoRequestFocus();
    _autoRequestFocus() {
        FocusScope.of(context).children.where((FocusNode node) => node.debugLabel == "phone_widgets").forEach((FocusNode node) {
          if (node.debugLabel == "phone_widgets") {
            if (_tabController.index == 1) {
              //里面会把child给remove掉,及全部child都失去焦点
              if(node.hasFocus) node.unfocus();
            } else {
              //该焦点组获取到焦点,至于焦点落在那个View上,看哪个焦点事件去请求。
              if(!node.hasFocus) FocusScope.of(context).requestFocus(node);
          } else {
            node.unfocus();
    
    顺带知识点
  • TabView中如果某个widget还存在焦点情况下,页切换至不可见也不会走dispose,应该类似与java的强引用一样,焦点树的根节点是一个强引用,不解除依赖关系页面不会被清除。