Flutter KeyEvent && Focus分析

本文主要内容是分析Flutter KeyEvent和焦点控制流程,适合有Flutter客户端或Framework开发经验的读者。

本文流程分析基于SDK:

Flutter version 1.12.13+hotfix.7

在上一篇文章Flutter在TV端的使用思考中已经分析过Flutter KeyEvent流程,Flutter 1.12.13相比以前,Focus焦点更为灵活,这里为了关联Focus,再次简单分析下KeyEvent流程,更细致的流程分析可以参考Flutter在TV端的使用思考

Flutter在客户端初始化时注册了对平台事件的hooks,比如PlatformViewAndroid,最终根据不同平台分发事件:

###dart:ui/hooks.dart
@pragma('vm:entry-point')
void _dispatchPlatformMessage(String name, ByteData data, int responseId) {
  if (name == ChannelBuffers.kControlChannelName) {
    try {
      channelBuffers.handleMessage(data);
    } catch (ex) {
      _printDebug('Message to "$name" caused exception $ex');
    } finally {
      window._respondToPlatformMessage(responseId, null);
    }
  } else if (window.onPlatformMessage != null) {
    _invoke3(
      window.onPlatformMessage,
      window._onPlatformMessageZone,
      name,
      data,
      (ByteData responseData) {
        window._respondToPlatformMessage(responseId, responseData);
      },
    );
  } else {
    channelBuffers.push(name, data, (ByteData responseData) {
      window._respondToPlatformMessage(responseId, responseData);
    });
  }
}

handleMessage经过层层转换:

  ###package:flutter/src/services/binding.dart
  ###_DefaultBinaryMessenger
  @override
  Future handlePlatformMessage(
    String channel,
    ByteData data,
    ui.PlatformMessageResponseCallback callback,
  ) async {
    ByteData response;
    try {
      final MessageHandler handler = _handlers[channel];
      if (handler != null) {
        response = await handler(data);
      } else {
        ui.channelBuffers.push(channel, data, callback);
        callback = null;
      }
    } catch (exception, stack) {
      FlutterError.reportError(FlutterErrorDetails(
        exception: exception,
        stack: stack,
        library: 'services library',
        context: ErrorDescription('during a platform message callback'),
      ));
    } finally {
      if (callback != null) {
        callback(response);
      }
    }
  }
  ...
  @override
  void setMessageHandler(String channel, MessageHandler handler) {
    if (handler == null)
      _handlers.remove(channel);
    else
      _handlers[channel] = handler;
    ui.channelBuffers.drain(channel, (ByteData data, ui.PlatformMessageResponseCallback callback) async {
      await handlePlatformMessage(channel, data, callback);
    });
  }

注册setMessageHandler后,才能转发handlePlatformMessage,接下来看看在哪注册的。

看到RawKeyboard的初始化:

###package:flutter/src/services/raw_keyboard.dart
class RawKeyboard {
  RawKeyboard._() {
    SystemChannels.keyEvent.setMessageHandler(_handleKeyEvent);
  }
  ...
  Future _handleKeyEvent(dynamic message) async {
    final RawKeyEvent event = RawKeyEvent.fromMessage(message);
    if (event == null) {
      return;
    }
    if (event.data is RawKeyEventDataMacOs && event.logicalKey == LogicalKeyboardKey.fn) {
      // On macOS laptop keyboards, the fn key is used to generate home/end and
      // f1-f12, but it ALSO generates a separate down/up event for the fn key
      // itself. Other platforms hide the fn key, and just produce the key that
      // it is combined with, so to keep it possible to write cross platform
      // code that looks at which keys are pressed, the fn key is ignored on
      // macOS.
      return;
    }
    if (event is RawKeyDownEvent) {
      _keysPressed.add(event.logicalKey);
    }
    if (event is RawKeyUpEvent) {
      _keysPressed.remove(event.logicalKey);
    }
    // Make sure that the modifiers reflect reality, in case a modifier key was
    // pressed/released while the app didn't have focus.
    _synchronizeModifiers(event);
    if (_listeners.isEmpty) {
      return;
    }
    for (ValueChanged listener in List>.from(_listeners)) {
      if (_listeners.contains(listener)) {
        listener(event);
      }
    }
  }

通过flutter和native事件注册通道platform_channel中转了一下:

  ###package:flutter/src/services/platform_channel.dart
  ###BasicMessageChannel

  /// Sets a callback for receiving messages from the platform plugins on this
  /// channel. Messages may be null.
  ///
  /// The given callback will replace the currently registered callback for this
  /// channel, if any. To remove the handler, pass null as the `handler`
  /// argument.
  ///
  /// The handler's return value is sent back to the platform plugins as a
  /// message reply. It may be null.
  void setMessageHandler(Future handler(T message)) {
    if (handler == null) {
      binaryMessenger.setMessageHandler(name, null);
    } else {
      binaryMessenger.setMessageHandler(name, (ByteData message) async {
        return codec.encodeMessage(await handler(codec.decodeMessage(message)));
      });
    }
  }

再看看在客户端定义的按键事件监听控件:

###package:flutter/src/widgets/raw_keyboard_listener.dart
class RawKeyboardListener extends StatefulWidget {

}
  ...
  void _attachKeyboardIfDetached() {
    if (_listening)
      return;
    RawKeyboard.instance.addListener(_handleRawKeyEvent);
    _listening = true;
  }
  ...
  void _handleRawKeyEvent(RawKeyEvent event) {
    if (widget.onKey != null)
      widget.onKey(event);
  }
}

至此,客户端定义的RawKeyboardListener就可以通过OnKey监听到按键事件了。


Flutter KeyListener.png

KeyEvent事件分析后,接下来分析下关联的Focus流程:

###package:flutter/src/widgets/raw_keyboard_listener.dart
class RawKeyboardListener extends StatefulWidget {
  ...
  @override
  _RawKeyboardListenerState createState() => _RawKeyboardListenerState();
}

class _RawKeyboardListenerState extends State {
  ...
  @override
  Widget build(BuildContext context) {
    return Focus(
      focusNode: widget.focusNode,
      autofocus: widget.autofocus,
      child: widget.child,
    );
  }
}

涉及到本文的另一个主角Focus

void _initNode() {
    if (widget.focusNode == null) {
      // Only create a new node if the widget doesn't have one.
      // This calls a function instead of just allocating in place because
      // _createNode is overridden in _FocusScopeState.
      _internalNode ??= _createNode();
    }
    _focusAttachment = focusNode.attach(context, onKey: widget.onKey);
    focusNode.skipTraversal = widget.skipTraversal ?? focusNode.skipTraversal;
    focusNode.canRequestFocus = widget.canRequestFocus ?? focusNode.canRequestFocus;
    _canRequestFocus = focusNode.canRequestFocus;
    _hasPrimaryFocus = focusNode.hasPrimaryFocus;

    // Add listener even if the _internalNode existed before, since it should
    // not be listening now if we're re-using a previous one because it should
    // have already removed its listener.
    focusNode.addListener(_handleFocusChanged);
  }

  ...
  void _handleFocusChanged() {
    final bool hasPrimaryFocus = focusNode.hasPrimaryFocus;
    final bool canRequestFocus = focusNode.canRequestFocus;
    if (widget.onFocusChange != null) {
      widget.onFocusChange(focusNode.hasFocus);
    }
    if (_hasPrimaryFocus != hasPrimaryFocus) {
      setState(() {
        _hasPrimaryFocus = hasPrimaryFocus;
      });
    }
    if (_canRequestFocus != canRequestFocus) {
      setState(() {
        _canRequestFocus = canRequestFocus;
      });
    }
  }
  ...
  @override
  Widget build(BuildContext context) {
    _focusAttachment.reparent();
    return _FocusMarker(
      node: focusNode,
      child: Semantics(
        focusable: _canRequestFocus,
        focused: _hasPrimaryFocus,
        child: widget.child,
      ),
    );
  }

flutter/src/widgets/focus_manager.dart

 void _applyFocusChange() {
    _haveScheduledUpdate = false;
    assert(_focusDebug('Refreshing focus state. Next focus will be $_nextFocus'));
    final FocusNode previousFocus = _primaryFocus;
    if (_primaryFocus == null && _nextFocus == null) {
      // If we don't have any current focus, and nobody has asked to focus yet,
      // then revert to the root scope.
      _nextFocus = rootScope;
    }
    if (_nextFocus != null && _nextFocus != _primaryFocus) {
      _primaryFocus = _nextFocus;
      final Set previousPath = previousFocus?.ancestors?.toSet() ?? {};
      final Set nextPath = _nextFocus.ancestors.toSet();
      // Notify nodes that are newly focused.
      _dirtyNodes.addAll(nextPath.difference(previousPath));
      // Notify nodes that are no longer focused
      _dirtyNodes.addAll(previousPath.difference(nextPath));
      _nextFocus = null;
    }
    if (previousFocus != _primaryFocus) {
      assert(_focusDebug('Updating focus from $previousFocus to $_primaryFocus'));
      if (previousFocus != null) {
        _dirtyNodes.add(previousFocus);
      }
      if (_primaryFocus != null) {
        _dirtyNodes.add(_primaryFocus);
      }
    }
    assert(_focusDebug('Notifying ${_dirtyNodes.length} dirty nodes:', _dirtyNodes.toList().map((FocusNode node) => node.toString())));
    for (FocusNode node in _dirtyNodes) {
      node._notify();
    }
    _dirtyNodes.clear();
    assert(() {
      if (_kDebugFocus) {
        debugDumpFocusTree();
      }
      return true;
    }());
  }

还记得之前分析过KeyEvent事件流程吗:

class RawKeyboard {
  RawKeyboard._() {
    SystemChannels.keyEvent.setMessageHandler(_handleKeyEvent);
  }
  ...
  Future _handleKeyEvent(dynamic message) async {
    final RawKeyEvent event = RawKeyEvent.fromMessage(message);
    if (event == null) {
      return;
    }
    if (event.data is RawKeyEventDataMacOs && event.logicalKey == LogicalKeyboardKey.fn) {
      // On macOS laptop keyboards, the fn key is used to generate home/end and
      // f1-f12, but it ALSO generates a separate down/up event for the fn key
      // itself. Other platforms hide the fn key, and just produce the key that
      // it is combined with, so to keep it possible to write cross platform
      // code that looks at which keys are pressed, the fn key is ignored on
      // macOS.
      return;
    }
    if (event is RawKeyDownEvent) {
      _keysPressed.add(event.logicalKey);
    }
    if (event is RawKeyUpEvent) {
      _keysPressed.remove(event.logicalKey);
    }
    // Make sure that the modifiers reflect reality, in case a modifier key was
    // pressed/released while the app didn't have focus.
    _synchronizeModifiers(event);
    if (_listeners.isEmpty) {
      return;
    }
    for (ValueChanged listener in List>.from(_listeners)) {
      if (_listeners.contains(listener)) {
        listener(event);
      }
    }
  }

事件是通过已注册的listener实现回调的。

有过Flutter客户端开发经验的读者知道,Flutter应用入口是:

void main() => runApp(App());
###package:flutter/src/services/binding.dart
void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();
}

class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
  static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null)
      WidgetsFlutterBinding();
    return WidgetsBinding.instance;
  }

...
abstract class BindingBase {
  BindingBase() {
    developer.Timeline.startSync('Framework initialization');

    assert(!_debugInitialized);
    initInstances();
    assert(_debugInitialized);

    assert(!_debugServiceExtensionsRegistered);
    initServiceExtensions();
    assert(_debugServiceExtensionsRegistered);

    developer.postEvent('Flutter.FrameworkInitialization', {});

    developer.Timeline.finishSync();
  }
  ...
}

在flutter客户端启动时的WidgetsBinding初始化:

/// The glue between the widgets layer and the Flutter engine.
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
    // Initialization of [_buildOwner] has to be done after
    // [super.initInstances] is called, as it requires [ServicesBinding] to
    // properly setup the [defaultBinaryMessenger] instance.
    _buildOwner = BuildOwner();
    buildOwner.onBuildScheduled = _handleBuildScheduled;
    window.onLocaleChanged = handleLocaleChanged;
    window.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
    SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
    FlutterErrorDetails.propertiesTransformers.add(transformDebugCreator);
  }
class BuildOwner {
  /// Creates an object that manages widgets.
  BuildOwner({ this.onBuildScheduled });
  ...
  FocusManager focusManager = FocusManager();
  ...
}

也就是说在Flutter应用启动初始化时。就绑定了FocusManager,正是FocusManager实例化时在RawKeyboard注册了listener:

class FocusManager with DiagnosticableTreeMixin {
  FocusManager() {
    rootScope._manager = this;
    RawKeyboard.instance.addListener(_handleRawKeyEvent);
    GestureBinding.instance.pointerRouter.addGlobalRoute(_handlePointerEvent);
  }
  ...
  void _handleRawKeyEvent(RawKeyEvent event) {
    // Update highlightMode first, since things responding to the keys might
    // look at the highlight mode, and it should be accurate.
    if (_lastInteractionWasTouch) {
      _lastInteractionWasTouch = false;
      _updateHighlightMode();
    }

    assert(_focusDebug('Received key event ${event.logicalKey}'));
    // Walk the current focus from the leaf to the root, calling each one's
    // onKey on the way up, and if one responds that they handled it, stop.
    if (_primaryFocus == null) {
      assert(_focusDebug('No primary focus for key event, ignored: $event'));
      return;
    }
    bool handled = false;
    for (FocusNode node in [_primaryFocus, ..._primaryFocus.ancestors]) {
      if (node.onKey != null && node.onKey(node, event)) {
        assert(_focusDebug('Node $node handled key event $event.'));
        handled = true;
        break;
      }
    }
    if (!handled) {
      assert(_focusDebug('Key event not handled by anyone: $event.'));
    }
  }

这里有个转换 , onKey到focusInDirection

###package:flutter/src/widgets/shortcuts.dart
class _ShortcutsState extends State {
  ...
  bool _handleOnKey(FocusNode node, RawKeyEvent event) {
    if (node.context == null) {
      return false;
    }
    return manager.handleKeypress(node.context, event) || manager.modal;
  }
###package:flutter/src/widgets/shortcuts.dart
class ShortcutManager extends ChangeNotifier with DiagnosticableMixin {
  ...
  @protected
  bool handleKeypress(
    BuildContext context,
    RawKeyEvent event, {
    LogicalKeySet keysPressed,
  }) {
    if (event is! RawKeyDownEvent) {
      return false;
    }
    assert(context != null);
    final LogicalKeySet keySet = keysPressed ?? LogicalKeySet.fromSet(RawKeyboard.instance.keysPressed);
    Intent matchedIntent = _shortcuts[keySet];
    if (matchedIntent == null) {
      // If there's not a more specific match, We also look for any keys that
      // have synonyms in the map.  This is for things like left and right shift
      // keys mapping to just the "shift" pseudo-key.
      final Set pseudoKeys = {};
      for (LogicalKeyboardKey setKey in keySet.keys) {
        final Set synonyms = setKey.synonyms;
        if (synonyms.isNotEmpty) {
          // There currently aren't any synonyms that match more than one key.
          pseudoKeys.add(synonyms.first);
        } else {
          pseudoKeys.add(setKey);
        }
      }
      matchedIntent = _shortcuts[LogicalKeySet.fromSet(pseudoKeys)];
    }
    if (matchedIntent != null) {
      final BuildContext primaryContext = primaryFocus?.context;
      if (primaryContext == null) {
        return false;
      }
      return Actions.invoke(primaryContext, matchedIntent, nullOk: true);
    }
    return false;
  }
###package:flutter/src/widgets/actions.dart
class Actions extends InheritedWidget {
  ...
  static bool invoke(
    BuildContext context,
    Intent intent, {
    FocusNode focusNode,
    bool nullOk = false,
  }) {
    assert(context != null);
    assert(intent != null);
    Element actionsElement;
    Action action;

    bool visitAncestorElement(Element element) {
      if (element.widget is! Actions) {
        // Continue visiting.
        return true;
      }
      // Below when we invoke the action, we need to use the dispatcher from the
      // Actions widget where we found the action, in case they need to match.
      actionsElement = element;
      final Actions actions = element.widget;
      action = actions.actions[intent.key]?.call();
      // Keep looking if we failed to find and create an action.
      return action == null;
    }

    context.visitAncestorElements(visitAncestorElement);
    assert(() {
      if (nullOk) {
        return true;
      }
      if (actionsElement == null) {
        throw FlutterError('Unable to find a $Actions widget in the context.\n'
            '$Actions.invoke() was called with a context that does not contain an '
            '$Actions widget.\n'
            'No $Actions ancestor could be found starting from the context that '
            'was passed to $Actions.invoke(). This can happen if the context comes '
            'from a widget above those widgets.\n'
            'The context used was:\n'
            '  $context');
      }
      if (action == null) {
        throw FlutterError('Unable to find an action for an intent in the $Actions widget in the context.\n'
            '$Actions.invoke() was called on an $Actions widget that doesn\'t '
            'contain a mapping for the given intent.\n'
            'The context used was:\n'
            '  $context\n'
            'The intent requested was:\n'
            '  $intent');
      }
      return true;
    }());
    if (action == null) {
      // Will only get here if nullOk is true.
      return false;
    }

    // Invoke the action we found using the dispatcher from the Actions Element
    // we found, using the given focus node.
    return _findDispatcher(actionsElement).invokeAction(action, intent, focusNode: focusNode);
  }
  ...
class ActionDispatcher extends Diagnosticable {
  ...
  bool invokeAction(Action action, Intent intent, {FocusNode focusNode}) {
    assert(action != null);
    assert(intent != null);
    focusNode ??= primaryFocus;
    if (action != null && intent.isEnabled(focusNode.context)) {
      action.invoke(focusNode, intent);
      return true;
    }
    return false;
  }
###flutter/src/widgets/focus_traversal.dart
class DirectionalFocusAction extends _RequestFocusActionBase {
  ...
  @override
  void invoke(FocusNode node, DirectionalFocusIntent intent) {
    if (!intent.ignoreTextFields || node.context.widget is! EditableText) {
      node.focusInDirection(intent.direction);
    }
  }

以上流程可以理解为中间调用,不作细节上的分析。
也终于看到了焦点控制的核心内容:

###package:flutter/src/widgets/focus_manager.dart
class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
 ...
 bool focusInDirection(TraversalDirection direction) => DefaultFocusTraversal.of(context).inDirection(this, direction);
 ...
}

mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
 ...
 bool inDirection(FocusNode currentNode, TraversalDirection direction) {
    final FocusScopeNode nearestScope = currentNode.nearestScope;
    final FocusNode focusedChild = nearestScope.focusedChild;
    if (focusedChild == null) {
      final FocusNode firstFocus = findFirstFocusInDirection(currentNode, direction) ?? currentNode;
      switch (direction) {
        case TraversalDirection.up:
        case TraversalDirection.left:
          _focusAndEnsureVisible(
            firstFocus,
            alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
          );
          break;
        case TraversalDirection.right:
        case TraversalDirection.down:
          _focusAndEnsureVisible(
            firstFocus,
            alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
          );
          break;
      }
      return true;
    }
    if (_popPolicyDataIfNeeded(direction, nearestScope, focusedChild)) {
      return true;
    }
    FocusNode found;
    final ScrollableState focusedScrollable = Scrollable.of(focusedChild.context);
    switch (direction) {
      case TraversalDirection.down:
      case TraversalDirection.up:
        Iterable eligibleNodes = _sortAndFilterVertically(
          direction,
          focusedChild.rect,
          nearestScope.traversalDescendants,
        );
        if (focusedScrollable != null && !focusedScrollable.position.atEdge) {
          final Iterable filteredEligibleNodes = eligibleNodes.where((FocusNode node) => Scrollable.of(node.context) == focusedScrollable);
          if (filteredEligibleNodes.isNotEmpty) {
            eligibleNodes = filteredEligibleNodes;
          }
        }
        if (eligibleNodes.isEmpty) {
          break;
        }
        List sorted = eligibleNodes.toList();
        if (direction == TraversalDirection.up) {
          sorted = sorted.reversed.toList();
        }
        // Find any nodes that intersect the band of the focused child.
        final Rect band = Rect.fromLTRB(focusedChild.rect.left, -double.infinity, focusedChild.rect.right, double.infinity);
        final Iterable inBand = sorted.where((FocusNode node) => !node.rect.intersect(band).isEmpty);
        if (inBand.isNotEmpty) {
          // The inBand list is already sorted by horizontal distance, so pick the closest one.
          found = inBand.first;
          break;
        }
        // Only out-of-band targets remain, so pick the one that is closest the to the center line horizontally.
        sorted.sort((FocusNode a, FocusNode b) {
          return (a.rect.center.dx - focusedChild.rect.center.dx).abs().compareTo((b.rect.center.dx - focusedChild.rect.center.dx).abs());
        });
        found = sorted.first;
        break;
      case TraversalDirection.right:
      case TraversalDirection.left:
        Iterable eligibleNodes = _sortAndFilterHorizontally(direction, focusedChild.rect, nearestScope);
        if (focusedScrollable != null && !focusedScrollable.position.atEdge) {
          final Iterable filteredEligibleNodes = eligibleNodes.where((FocusNode node) => Scrollable.of(node.context) == focusedScrollable);
          if (filteredEligibleNodes.isNotEmpty) {
            eligibleNodes = filteredEligibleNodes;
          }
        }
        if (eligibleNodes.isEmpty) {
          break;
        }
        List sorted = eligibleNodes.toList();
        if (direction == TraversalDirection.left) {
          sorted = sorted.reversed.toList();
        }
        // Find any nodes that intersect the band of the focused child.
        final Rect band = Rect.fromLTRB(-double.infinity, focusedChild.rect.top, double.infinity, focusedChild.rect.bottom);
        final Iterable inBand = sorted.where((FocusNode node) => !node.rect.intersect(band).isEmpty);
        if (inBand.isNotEmpty) {
          // The inBand list is already sorted by vertical distance, so pick the closest one.
          found = inBand.first;
          break;
        }
        // Only out-of-band targets remain, so pick the one that is closest the to the center line vertically.
        sorted.sort((FocusNode a, FocusNode b) {
          return (a.rect.center.dy - focusedChild.rect.center.dy).abs().compareTo((b.rect.center.dy - focusedChild.rect.center.dy).abs());
        });
        found = sorted.first;
        break;
    }
    if (found != null) {
      _pushPolicyData(direction, nearestScope, focusedChild);
      switch (direction) {
        case TraversalDirection.up:
        case TraversalDirection.left:
          _focusAndEnsureVisible(
            found,
            alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
          );
          break;
        case TraversalDirection.down:
        case TraversalDirection.right:
        _focusAndEnsureVisible(
          found,
          alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
        );
          break;
      }
      return true;
    }
    return false;
  }
}

所有焦点的控制都在这个方法inDirection里。
寻找焦点的逻辑比较复杂,可以分为两种情况:
1.如果有记录的最近的可用焦点,找到方向最近的第一个:

  @override
  FocusNode findFirstFocusInDirection(FocusNode currentNode, TraversalDirection direction) {
    assert(direction != null);
    assert(currentNode != null);
    switch (direction) {
      case TraversalDirection.up:
        // Find the bottom-most node so we can go up from there.
        return _sortAndFindInitial(currentNode, vertical: true, first: false);
      case TraversalDirection.down:
        // Find the top-most node so we can go down from there.
        return _sortAndFindInitial(currentNode, vertical: true, first: true);
      case TraversalDirection.left:
        // Find the right-most node so we can go left from there.
        return _sortAndFindInitial(currentNode, vertical: false, first: false);
      case TraversalDirection.right:
        // Find the left-most node so we can go right from there.
        return _sortAndFindInitial(currentNode, vertical: false, first: true);
    }
    return null;
  }

例如up方向,寻找焦点list中bottom坐标最小的控件:

 FocusNode _sortAndFindInitial(FocusNode currentNode, { bool vertical, bool first }) {
    final Iterable nodes = currentNode.nearestScope.traversalDescendants;
    final List sorted = nodes.toList();
    sorted.sort((FocusNode a, FocusNode b) {
      if (vertical) {
        if (first) {
          return a.rect.top.compareTo(b.rect.top);
        } else {
          return b.rect.bottom.compareTo(a.rect.bottom);
        }
      } else {
        if (first) {
          return a.rect.left.compareTo(b.rect.left);
        } else {
          return b.rect.right.compareTo(a.rect.right);
        }
      }
    });

    if (sorted.isNotEmpty)
      return sorted.first;

    return null;
  }

找到了可用焦点,还需要确保焦点可见:

@protected
void _focusAndEnsureVisible(FocusNode node, {ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit}) {
  node.requestFocus();
  Scrollable.ensureVisible(node.context, alignment: 1.0, alignmentPolicy: alignmentPolicy);
}
  void requestFocus([FocusNode node]) {
    if (node != null) {
      if (node._parent == null) {
        _reparent(node);
      }
      assert(node.ancestors.contains(this), 'Focus was requested for a node that is not a descendant of the scope from which it was requested.');
      node._doRequestFocus();
      return;
    }
    _doRequestFocus();
  }

  ...
  void _doRequestFocus() {
    if (!canRequestFocus) {
      assert(_focusDebug('Node NOT requesting focus because canRequestFocus is false: $this'));
      return;
    }
    _setAsFocusedChild();
    if (hasPrimaryFocus) {
      return;
    }
    _hasKeyboardToken = true;
    assert(_focusDebug('Node requesting focus: $this'));
    _markAsDirty(newFocus: this);
  }

至于Scrollable.ensureVisible,控件支持Scrollable才有效,比如PageView,ListView,GridView等。

2.如果记录中没有找临近的,则寻找亲近的焦点,例如up方向上先寻找焦点的中心Y方向上坐标小于当前焦点top坐标的所有焦点:

  Iterable _sortAndFilterVertically(
    TraversalDirection direction,
    Rect target,
    Iterable nodes,
  ) {
    final List sorted = nodes.toList();
    sorted.sort((FocusNode a, FocusNode b) => a.rect.center.dy.compareTo(b.rect.center.dy));
    switch (direction) {
      case TraversalDirection.up:
        return sorted.where((FocusNode node) => node.rect != target && node.rect.center.dy <= target.top);
      case TraversalDirection.down:
        return sorted.where((FocusNode node) => node.rect != target && node.rect.center.dy >= target.bottom);
      case TraversalDirection.left:
      case TraversalDirection.right:
        break;
    }
    assert(direction == TraversalDirection.up || direction == TraversalDirection.down);
    return null;
  }

然后找到所有有效焦点中Y方向坐标最大,且X方向上最近的一个

if (direction == TraversalDirection.up) {
  sorted = sorted.reversed.toList();
}
...
sorted.sort((FocusNode a, FocusNode b) {
  return (a.rect.center.dx - focusedChild.rect.center.dx).abs().compareTo((b.rect.center.dx - focusedChild.rect.center.dx).abs());
});
found = sorted.first;

找到后再_focusAndEnsureVisible。
总结了一张简单的流程图:


Flutter Focus.png

结束语:
本文把Flutter的KeyEvent和Focus结合在一起分析了下流程,覆盖面并不全面,有遗漏或者错误的地方欢迎指正。

你可能感兴趣的:(Flutter KeyEvent && Focus分析)