本文主要内容是分析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监听到按键事件了。
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的KeyEvent和Focus结合在一起分析了下流程,覆盖面并不全面,有遗漏或者错误的地方欢迎指正。