likes
comments
collection
share

如何优雅地解决flutter桌面开发中的方向快捷键的失焦问题 ?在flutter的桌面端开发中,键盘事件监听器RawKe

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

在flutter的桌面端开发中,键盘事件监听器RawKeyboardListener可以用来实现对键盘事件的监听,广泛用于处理快捷键。 然而键盘事件监听器为方向键与屏幕上的焦点Focus的变化配置了默认行为,这会导致我们监听的方向键触发事件被打断,无法获得预期表现。 本文将简要介绍Flutter的键盘监听机制、其对方向快捷键做了哪些默认的行为配置,以及如何覆盖默认的焦点变化事件,来解决业务中遇到的失焦问题。

1.业务场景

在flutter桌面端的业务开发中,为了提升用户的输入体验,通常要对各类快捷键进行细致的处理,比如在搜索选项表中,需要对搜索下拉菜单添加上下方向键来控制选择的项目,效果如下图所示: 如何优雅地解决flutter桌面开发中的方向快捷键的失焦问题 ?在flutter的桌面端开发中,键盘事件监听器RawKe 预想的实现非常简单,只需要通过键盘事件监听器RawKeyboardListener监听上下方向键的监听就可以完成此功能,示例代码如下:

child: RawKeyboardListener(
	focusNode: FocusNode(),
	onKey: (event) {
	  if (event.runtimeType.toString() ==
			  'RawKeyDownEvent') {
		if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
		  handleKeyDown();
		}
		if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
		  handleKeyUp();
		}
	  }
	},
)

但实际上在flutter中,对方向键的触发做了焦点查找的默认行为:我们的页面有多个封装后的输入框,这时按下方向键其会去寻找其他方向上可用的焦点,预期是触发我们的监听事件,但在此之前当前的输入框已经失焦,因此看不到预期效果,表现如下图所示:

如何优雅地解决flutter桌面开发中的方向快捷键的失焦问题 ?在flutter的桌面端开发中,键盘事件监听器RawKe 由此我们需要知道Flutter的键盘监听过程是怎样的,又做了哪些默认的行为处理,以解决我们需要保持焦点的问题。

2.RawKeyBoardListener监听流程

在flutter的src目录下文件raw_keyboard_listener.dart里,我们可以看到如下代码,通过_handleRawKeyEvent来调用我们可以定义的监听回调onKey,_attachKeyboardIfDetached通过RawKeyboardaddListenner将其注册监听。

class _RawKeyboardListenerState extends State<RawKeyboardListener> {
  ...
	void _attachKeyboardIfDetached() {
		if (_listening)
		  return;
		RawKeyboard.instance.addListener(_handleRawKeyEvent);
		_listening = true;
	}
	...
	void _handleRawKeyEvent(RawKeyEvent event) {
		widget.onKey?.call(event);
	}
	...
}

然后在raw_keyboard.dart中,addlistener将参数中的事件全部加入_listsener,当handleRawKeyEvent被调用时(键盘事件回调),就会分发执行注册的listener。 最终经过层层转换将keyEvent作为一个message事件来进行监听工作,注册和回调流程如图下图所示,感兴趣的同学可以自行到对应的文件中阅读源码。 如何优雅地解决flutter桌面开发中的方向快捷键的失焦问题 ?在flutter的桌面端开发中,键盘事件监听器RawKe

3.键盘方向键的默认行为

由上面的过程我们可以知道,我们的监听事件是在RawKeyboardListener装载时被注册的,但是在Flutter的入口文件app.dart中,我们发现如果我们没有配置快捷键Widgetshortcuts,会有默认的WidgetsApp.defaultShortcuts快捷键规则:

class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
...
return RootRestorationScope(
      restorationId: widget.restorationScopeId,
      child: Shortcuts(
        debugLabel: '<Default WidgetsApp Shortcuts>',
        shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts, // 这里配置快捷键
        // DefaultTextEditingShortcuts is nested inside Shortcuts so that it can
        // fall through to the defaultShortcuts.
        child: DefaultTextEditingShortcuts(
          child: Actions(
            actions: widget.actions ?? WidgetsApp.defaultActions,
            child: DefaultTextEditingActions(
              child: FocusTraversalGroup(
                policy: ReadingOrderTraversalPolicy(),
                child: child,
              ),
            ),
          ),
        ),
      ),
    );
...
}

这个规则会根据当前的设备进行分发,平台有androidiOSmacOSlinuxwindows等。 此处我们开发的是mac平台的应用,因此查看macOS的默认事件,发现四个方向键均配置有DirectionlFocusIntent也就是说默认情况下,方向键会触发这个焦点变化的函数。

// Default shortcuts for the macOS platform.
  static const Map<ShortcutActivator, Intent> _defaultAppleOsShortcuts = <ShortcutActivator, Intent>{
    ...
    SingleActivator(LogicalKeyboardKey.arrowLeft): DirectionalFocusIntent(TraversalDirection.left),
    SingleActivator(LogicalKeyboardKey.arrowRight): DirectionalFocusIntent(TraversalDirection.right),
    SingleActivator(LogicalKeyboardKey.arrowDown): DirectionalFocusIntent(TraversalDirection.down),
    SingleActivator(LogicalKeyboardKey.arrowUp): DirectionalFocusIntent(TraversalDirection.up),

    // Scrolling
	...

焦点控制的核心部分是focus_traversal.dart文件中的inDirection方法,感兴趣的同学可以去自行查阅源码,这里我们简单总结一下该函数寻找焦点的逻辑:

  • 如果有记录的最近的可用焦点,找到方向最近的第一个(按照各方向的按键来寻找),找到焦点后需要保证焦点可见,即滚动位置让聚焦的focusNode在可视区域内。
  • 如果记录中没有找临近的,则寻找位置上相近的焦点。例如下方向键⬇️,先寻找焦点的中心Y方向上坐标小于当前焦点Y坐标的所有焦点,然后在这些有效焦点中,选出Y方向坐标最大,且X方向上差值最小的焦点,将焦点控制权转移至该焦点。

经过这一番探寻,我们终于找到了为什么焦点会向下移动:是因为我们采用键盘事件监听器RawKeyboardListener对键盘事件进行处理,没有配置Shortcut快捷键,所以flutter为我们配置了默认的快捷键。

4.保持焦点

4.1 常规做法

在flutter2.5中更新了有关快捷键支持的问题,DefaultTextEditingShortcuts 类包含每个平台上受支持的键盘快捷键列表,如果开发者想覆盖任何内容,可以使用 Flutter 的现有 Shortcuts 将任何快捷方式重新映射到现有或自定义意图。于是我们可以用Shortcuts部件包裹textfield,在对应的属性传入自定义的快捷键声明,并用Actions定义具体的快捷键和对应的动作。于是我们只需要定义arrowDownarrowUp的对应操作,就可以覆盖原有的默认操作,从而将焦点保持在当前输入框:

Shortcuts(
	shortcuts: <ShortcutActivator, Intent>{
		SingleActivator(LogicalKeyboardKey.arrowDown):
			IncrementIndexIntent(),
		SingleActivator(LogicalKeyboardKey.arrowUp):
			DecrementIndexIntent(),
	},
	child: Actions(
		actions: <Type, Action<Intent>>{
			IncrementIndexIntent:
				CallbackAction<IncrementIndexIntent>(
					onInvoke: (IncrementIndexIntent intent) {
						testIndex.value++;
					},
				),
			DecrementIndexIntent:
				CallbackAction<DecrementIndexIntent>(
					onInvoke: (DecrementIndexIntent intent) {
						testIndex.value--;
					},
				),
		},
		child: Textfield(,,,),
	),
)

4.2 特殊处理

然而对于我们的业务需求来说,事情没有这么简单。 在本需求中,输入框并非Flutter提供的原生输入框,由于要满足Tag输入、富文本等特殊场景,我们引入了extend_text_field,这个第三方库中很多快捷键并非使用ShortCuts实现,因此我们在外层定义ShortCuts,会使其内部的事件监听失效,导致该组件无法正常工作,由于该库面向移动端设计,对于桌面端相关的表现关注较少,短时间内难以修复此种异常,这种情况在Flutter桌面端开发中相当普遍。 权衡之下,我们决定暂时先不采取覆盖ShortCuts这一常见做法,这里我们介绍一种使用RawKeyboardListener让焦点重新回归的方案。 当按下上下键的时候,如我们在第一节所述,会先后调用keyDown和keyUp的onkey回调事件,具体流程如下图所示。

如何优雅地解决flutter桌面开发中的方向快捷键的失焦问题 ?在flutter的桌面端开发中,键盘事件监听器RawKe

由于默认的焦点变化事件DirectionalFocusIntent()会在keyDown的时候执行,找到下一个可用焦点后会让当前widget失去焦点,新widget获得焦点。完成后keyUp触发。于是我们找回焦点的关键就是在旧widget失去焦点的时候监听到该事件并且使用requestFocus找回焦点。 具体做法是:

  • 在旧widget的focus上绑定焦点监听事件listenFocus,并设置widgetHasFocus标记当前widget是否有焦点,isKeydown标记keyDown事件是否发生,当keydown动作触发时将其记为true。
  • 在焦点监听到变化时,如果keyDown为true,并且widgetHasFocus为true,而焦点的.hasFocus获取的结果为false,说明已经进行完unfocus操作,此时让当前焦点使用requestFocus抢回焦点,并在keyUp时将标记位还原,从而能够实现焦点的回归。

5.总结

经过以上的探索过程,相信读者已经对Flutter中桌面端的键盘事件机制及焦点查找行为有了一定了解,上面提到的源码文件中也有关于其他各平台的键盘监听机制的定义,希望这条完整的探索路径可以为遇到类似问题的同学带来启发。

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