The widgets and feature classes of the Focus series are the unsung heroes of Flutter, quietly contributing but not widely known. You don’t use it much in your daily development use. Why is that? So with that question in mind we start today.

1.Focus on relevant introduction

Here are some Focus related widgets and feature classes to help you understand the Focus Tree section. This source code is based on 1.20.0-2.0.pre.

1.1 FocusNode

FocusNode is the object used by widgets to get keyboard focus and handle keyboard events. It is inherited from ChangeNotifier, so we can get the corresponding FocusNode information anywhere.

Here are some common methods for FocusNode:

  • RequestFocus is used as the requestFocus. Note that the execution of this requestFocus is placed in scheduleMicrotask, so results may be delayed by up to one frame.

  • Unfocus is used as unfocus and the default behavior is unfocusfocusdisposition. Scope:

void unfocus({UnfocusDisposition disposition = UnfocusDisposition.scope,}) {... }Copy the code

The UnfocusDisposition enumeration class is the behavior after focus is removed and is divided into scope and previouslyFocusedChild.

  1. Scope means to look up the most recent FocusScopeNode.

  2. PreviouslyFocusedChild looks for the last focus location and, if not, gives the current FocusScopeNode.

Concrete implementation visible unfocus source code, here is not much to say.

  • disposeThere’s nothing to be said for this. Use it carefullyFocusNodeDestroy immediately after completion.

1.2 FocusScopeNode

FocusScopeNode is a subclass of FocusNode. It organizes focusNodes into a scope, forming a set of nodes that can be traversed. It provides the last FocusNode(focusedChild) to get focus, and if focus is removed from one of the nodes, the FocusScopeNode gets focus again, and _focusedChildren clears.

  /// Returns the child of this node that should receive focus if this scope
  /// node receives focus.
  ///
  /// If [hasFocus] is true, then this points to the child of this node that is
  /// currently focused.
  ///
  /// Returns null if there is no currently focused child.
  FocusNode get focusedChild {
    return _focusedChildren.isNotEmpty ? _focusedChildren.last : null;
  }

  // A stack of the children that have been set as the focusedChild, most recent
  // last (which is the top of the stack).
  final List<FocusNode> _focusedChildren = <FocusNode>[];
Copy the code

Note that _focusedChildren is not all focusnodes that appear under FocusScopeNode, but only focusnodes that have been focussed. The source code is as follows:

  void _setAsFocusedChildForScope(a) {
    FocusNode scopeFocus = this;
    for (final FocusScopeNode ancestor in ancestors.whereType<FocusScopeNode>()) {
      // Remove from focus history
      ancestor._focusedChildren.remove(scopeFocus);
      // Add it to the end so that the focusedChild above gets the last node to get focusancestor._focusedChildren.add(scopeFocus); scopeFocus = ancestor; }}Copy the code

The most important method of FocusScopeNode is setFirstFocus, which sets the sub-scoped node.

  void setFirstFocus(FocusScopeNode scope) {
    if (scope._parent == null) {
      Scope has no parent node. Add scope to the current node
      _reparent(scope);
    }
    if (hasFocus) {
      _doRequestFocus moves the focus to scope and records the node.
      scope._doRequestFocus(findFirstFocus: true);
    } else {
      // The current scope has no focus, record node.scope._setAsFocusedChildForScope(); }}Copy the code

1.3 the Focus

Focus is a Widget that can be used to assign Focus to itself and its child widgets. A FocusNode is managed internally, listening for changes in focus to keep the focus hierarchy in sync with the Widget hierarchy.

The most common InkWell uses it, and a lot of widgets like Buttons and chips use InkWell, so Focus is everywhere.

Let’s take a look at the InkResponse source:

Focus
onFocusChange

  void _handleFocusUpdate(bool hasFocus) {
    _hasFocus = hasFocus;
    _updateFocusHighlights();
    if(widget.onFocusChange ! =null) { widget.onFocusChange(hasFocus); }}Copy the code

To change the _hasFocus value when the focus changes, call the _updateFocusHighlights method.

  void _updateFocusHighlights(a) {
    bool showFocus;
    switch (FocusManager.instance.highlightMode) {
      case FocusHighlightMode.touch:
        showFocus = false;
        break;
      case FocusHighlightMode.traditional:
        showFocus = _shouldShowFocus;
        break;
    }
    updateHighlight(_HighlightType.focus, value: showFocus);
  }
Copy the code

The updateHighlight method is finally called to give the WIdget a highlight when it gets focus.

There is an enumerated class, FocusHighlightMode, which indicates which interaction mode is used to get the focus. There are touch and traditional.

The default distinction is as follows:

  static FocusHighlightMode get _defaultModeForPlatform {
    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.iOS:
        if (WidgetsBinding.instance.mouseTracker.mouseIsConnected) {
          return FocusHighlightMode.traditional;
        }
        return FocusHighlightMode.touch;
      case TargetPlatform.linux:
      case TargetPlatform.macOS:
      case TargetPlatform.windows:
        return FocusHighlightMode.traditional;
    }
    return null;
  }
Copy the code

The mobile side is touch without a mouse, and the desktop side is traditional (keyboard and mouse).

So that answers my question at the beginning. We generally only think about mobile devices, the Touch part, where we don’t really need the focus effect for buttons, maybe for apps like the Android TV box. The widgets that Flutter provides take into account the effects of each platform, which is why they are used. Similar to the InkResponse source code, MouseRegion Widget also appears. It tracks the mouse movement, such as moving the mouse over a button on the Web, and the button will have a change effect.

1.4 FocusScope

FocusScope is similar to Focus, but internally manages the FocusScopeNode. It does not change the primary focus, it just changes the scoped node that receives the focus. This is not used much in the source code, but it is an important place.

Take Navigator and Route. First, Navigator has a FocusScope that automatically gets focus. FocusScope is also added to each route it hosts so that the FocusScope can be moved to it when the page jumps to the /Dialog Dialog (using the setFirstFocus method).

Same thing with drawers. When the Drawer opens, our FocusScope moves to the Drawer, so we use the FocusScope as well.

If we want to manage focus, we have a Stack on the page, and the top layer overlays the bottom widgets, rendering them unoperable. At this point we can use the FocusScope to move the FocusScope up.

2.Focus Tree

There are a variety of “trees” in Flutter according to their classification, such as the Widget Tree, Element Tree and RenderObject Tree, and the other trees, such as the Semantics Tree mentioned in my previous blog, And Focus Tree.

The Focus Tree is a relatively simple Tree independent of the Widget Tree. It maintains the hierarchical relationship between the focused widgets in the Widget Tree. Since FocusTree cannot be visually observed by tools, we can print it using the debugDumpFocusTree method in FocusManager, the management class of FocusTree.

So I’m going to create a new project, and I’m going to write a little example here. The code is simple, a TextField and a FlatButton in Column.

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Column(
        children: [
          TextField(),
          FlatButton(
            child: Text('print FocusTree'), onPressed: () { WidgetsBinding.instance.addPostFrameCallback((_) { debugDumpFocusTree(); }); },),],),); }}Copy the code

Click the button to print the following result:

FocusManager#4148c │ UPDATE SCHEDULED │ primaryFocus: FocusScopeNode#af55c(_ModalScopeState<dynamic> │ FocusScope [PRIMARY Focus]) │ Focus: FocusScopeNode#4f0d5(Navigator Scope [IN FOCUS PATH]) │ primaryFocusCreator: FocusScope ← _ActionsMarker ← Actions ← │ PageStorage ← Offstage ← _ModalScopeStatus ← │ _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>># BFB70] ← _EffectiveTickerMode ← TickerMode ← │ _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState># 3FA85] │ ← _Theatre ← Overlay-[LabeledGlobalKey<OverlayState># 2D724] ← │ _FocusMarker ← Semantics ← FocusScope ← AbsorbPointer ← │ _PointerListener ← Listener ← HeroControllerScope ← │ Navigator-[GlobalObjectKey<NavigatorState> │ _WidgetsAppState # 9404 f] please.. │ └ ─ rootScope: FocusScopeNode#185ad(Root FocusScope [IN Focus PATH]) │ IN Focus PATH focusedChildren: Focusscopenod #4f0d5(Navigator Scope [IN FOCUS │ PATH]) │ ├ ─Child 1: └ (Shortcuts [IN FOCUS PATH]) │ FOCUS │ NOT FOCUSABLE │ IN FOCUS PATH │ ├ ─Child 1: FocusNode#1cd76(FocusTraversalGroup [IN FOCUS PATH]) │ FOCUS │ NOT FOCUSABLE IN FOCUS PATH │ ├ ─Child 1: Focusscoped #4f0d5(Navigator Scope [IN FOCUS PATH]) │ ├ ─ imp 1: FocusScopeNode#af55c(_ModalScopeState<dynamic> FocusScope [PRIMARY Focus]) │ context: FocusScope │ PRIMARY FOCUS │ ├─Child 1: FocusNode#e72e2 │ Context: EditableText-[LabeledGlobalKey<EditableTextState>#c2f8a] │ ├ ─Child 2: FocusNode#0b7c0 context: FocusCopy the code

Let me tell you what it means from the bottom up:

  1. Child 1: FocusNode# e72E2 and Child 2: FocusNode#0b7c0 are the same, representing TextField and FlatButton.

  2. The previous layer FocusScopeNode#af55c is the current page and you can see that the FOCUS is currently on it (PRIMARY FOCUS). It is created by calling _ModalScope in the MaterialPageRoute -> PageRoute -> ModalRoute ->createOverlayEntries -> _buildModalScope method.

  3. FocusScopeNode#4f0d5 is the Navigator with the following code:

final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'Navigator Scope');

@override
  Widget build(BuildContext context) {
    return HeroControllerScope(
      child: Listener(
        onPointerDown: _handlePointerDown,
        onPointerUp: _handlePointerUpOrCancel,
        onPointerCancel: _handlePointerUpOrCancel,
        child: AbsorbPointer(
          absorbing: false,
          child: FocusScope(
            node: focusScopeNode, // <---
            autofocus: true,
            child: Overlay(
              key: _overlayKey,
              initialEntries: overlay == null ?  _allRouteOverlayEntries.toList(growable: false) : const <OverlayEntry>[],
            ),
          ),
        ),
      ),
    );
  }
Copy the code
  1. Two floors upWidgetsApptheShortcutsandFocusTraversalGroupCreated.

  1. The top layer isrootScopeIt is inWidgetsBindingCalled when initializedBuildOwnercreateFocusManager.
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
  @override
  void initInstances(a) {
    super.initInstances(); _buildOwner = BuildOwner(); . }... }Copy the code
class BuildOwner {
  /// Creates an object that manages widgets.
  BuildOwner({ this.onBuildScheduled });

  /// The object in charge of the focus tree.FocusManager focusManager = FocusManager(); . }Copy the code
class FocusManager with DiagnosticableTreeMixin.ChangeNotifier {
  final FocusScopeNode rootScope = FocusScopeNode(debugLabel: 'Root Focus Scope');
  
  FocusManager() {
    rootScope._manager = this; . }... }Copy the code
  1. Finally,FocusManagerClass information.
  • primaryFocus: The current primary focus.
  • rootScope: Indicates the root node of the current Focus Tree.
  • highlightMode: The current interaction mode for getting focus, as mentioned above.
  • highlightStrategy: Interactive mode policy, defaultautomaticAutomatically switches based on the last input received. You can also specify one way or the other.
  • FocusManagerAlso inherit fromChangeNotifierSo we can passaddListenerListening to theprimaryFocusThe change.

3. The Tree change Focus

Now I first click on the input box and then click on the button to print the following result (just take the last few layers) :

primaryFocus: FocusNode#e72e2([PRIMARY FOCUS]) ... ├ ─Child 1: focusScopenode1 (_ModalScopeState<dynamic> FocusScope [IN Focus PATH]) │ ├ ─ imp. FocusScope │ FOCUS PATH │ focusedChildren # e72E2 ([PRIMARY FOCUS]) │ ├─Child 1: FocusNode# e72e2 ([PRIMARY FOCUS]) │ context: EditableText-[LabeledGlobalKey<EditableTextState>#c2f8a] │ PRIMARY FOCUS │ ├ ─Child 2: FocusNode#0b7c0 context: FOCUSCopy the code

You can see that the current focus primaryFocus is FocusNode#e72e2, which is on the TextField. Notice that focusedChildren here only has FocusNode#e72e2 at this point.

Because I click on TextField, and the soft keyboard pops up. Now I need to turn off the soft keyboard. Here are four methods:

  1. Using SystemChannels) (see) the invokeMethod (‘. (hide) method, this method after close the soft keyboard focus remains the same, also on the TextField, so there is a problem. If you push to a new page and pop back, the soft keyboard will pop up again. It is not recommended.

  2. Use the focusScope.of (context).requestFocus(FocusNode()) method and print the Focus Tree.

├ ─ 2 ([PRIMARY FOCUS]) ├ ─ 2: FocusScopeNode#af55c(_ModalScopeState<dynamic> FocusScope [IN Focus PATH]) │ context: FocusScope │ FOCUS PATH │ focusedChildren: FocusNode#7da34([PRIMARY FOCUS]), │ FocusNode#e72e2 │ ├─Child 1: FocusNode# e72E2 │ context: EditableText-[LabeledGlobalKey<EditableTextState># c2F8A] │ ├─Child 2: │ ├ ─ imp. 2 ([PRIMARY Focus]) PRIMARY FocusCopy the code

You can see that a FocusNode#7da34 is actually created under the current node and the focus is shifted to it. Notice that focusedChildren now have FocusNode#7da34 and FocusNode#e72e2.

  1. useFocusScope.of(context).unfocus()Method repeat the above steps and printFocus Tree.
├ ─ 2 (Navigator Scope [PRIMARY FOCUS]) └─Child 1: Focusscoped #4f0d5(Navigator Scope [PRIMARY FOCUS]) │ ├ ─ imp 1: FocusScopeNode#af55c(_ModalScopeState<dynamic> FocusScope) │ context: FocusScope │ focusedChildren: FocusNode#e72e2, FocusNode# 7DA34 │ ├─Child 1: FocusNode#e72e2 │ Context: EditableText-[LabeledGlobalKey<EditableTextState># c2f8A] │ ├─Child 2: Focus ├─Child 3: FocusNode#7da34Copy the code

FocusScopeNode#af55c = FocusScopeNode#af55c

Because the focusScope. of(context) method returns the FocusScopeNode as the FocusScopeNode#af55c of the current page, when you unfocus it, the focus will now look up to the Navigator.

Notice that focusedChildren now have FocusNode#e72e2 and FocusNode#7da34. But do you see a problem here? The focus is no longer in the scope of FocusScopeNode#af55c, but there is still data in focusedChildren. If we use methods such as focusscope.of (context).focusedchild, the results will be incorrect.

It is safe to use the fourth method below.

  1. The last way is to giveTextFieldAdd attributesfocusNode, directly call_focusNode.unfocus():
final FocusNode _focusNode = FocusNode();
TextField(
  focusNode: _focusNode,
),
_focusNode.unfocus();
Copy the code

I won’t post the result here, which is roughly the same as the original, with focusedChildren empty and not printed. This successfully returns focus to the parent scope (the current page), although this can be cumbersome if the page is complex and you need to manage it with each added FocusNode. Therefore, it is recommended to use:

FocusManager.instance.primaryFocus? .unfocus();Copy the code

It can get the current focus directly, so that we can cancel the focus directly. So the comparison of these four methods, certainly the latter is better, but also avoid other hidden dangers caused by data errors.

4. Conclusion

By observing the changes of the Focus Tree, we can roughly understand the composition and change rules of the Focus Tree. If you have the need to control the Focus, this article may bring you help.

In fact, there are many details about Focus, such as how FocusAttachment manages FocusNode and achieves FocusTraversalGroup of traversal order of FocusNode. Because space is limited, here is not introduced, interested can look at the source code.

This is the fourth in a series of talk about it, with links to the first three:

  • Talk about RepaintBoundary in Flutter

  • Talk about the Semantics of Flutter

  • Talk about Key, the most familiar stranger in Flutter

If this article has helped or inspired you, please feel free to give it a thumbs up. Also support my Open source Flutter project flutter_deer.

We’ll see you next month

Reference 5.

  • flutter issues 54277