原文地址:blog.gskinner.com/archives/20…
原文作者:blog.gskinner.com/archives/au…
发布时间:2021年6月11日
当在Flutter中构建一个自定义的UI控件时,很有可能只是使用一个GestureDetector就结束了,但这样做将是一个错误!尤其是当您打算支持用户使用鼠标或键盘等其他输入方式时。尤其是当您打算支持用户使用鼠标或键盘等其他输入方式时。
事实证明,制作一个对各种输入都表现 “正确 “的UI控件要比仅仅检测敲击动作复杂得多。一般来说,你创建的每个控件都需要以下功能。
- 悬停状态
- 一个焦点状态(用于TAB键或箭头键的遍历)。
- 一个可以改变鼠标光标的区域
- 用于[空格]和[回车]键(或者其他键)的处理程序
传统上,你可以通过组成一个大的部件块来创建,包括Focus、Actions、Shortcuts和MouseRegion。这很有效,但有很多模板和缩进。幸运的是,Flutter为这个目的提供了一个专门的小部件。FocusableActionDetector。
FocusableActionDetector将Focus、Actions、Shortcuts和MouseRegion融为一体,在Flutter SDK中被大量使用,包括Material DatePicker、Switch、Checkbox、Radio和Slider控件。
自己使用它是相当简单的。让我们来看看我们如何使用这个小部件建立一个完全自定义的按钮。
注意:要跟上代码,请查看这里的gist:gist.github.com/esDotDev/04…
MyCustomButton
首先,创建一个StatefulWidget,它可以保存你的_isFocused和_isHovered状态。我们还将创建一个FocusNode,我们可以用它来请求按下时的焦点,这是按钮的一个常见行为。还要创建一些典型的onPressed和标签字段来配置按钮。
class MyCustomButton extends StatefulWidget {
@override
State<MyCustomButton> createState() => _MyCustomButtonState();
}
class _MyCustomButtonState extends State<MyCustomButton> {
bool _isHovered = false;
bool _isFocused = false;
FocusNode _focusNode = FocusNode();
@override
Widget build(BuildContext context) {
// TODO:
}
void _handlePressed() {
_focusNode.requestFocus();
widget.onPressed();
}
}
复制代码
因为我们正在制作一个按钮,所以我们将添加一些基本的配置选项。
const MyCustomButton({Key? key, required this.onPressed, required this.label}) : super(key: key);
final VoidCallback onPressed;
final String label;
复制代码
现在剩下的就是填写build()方法了。
Widget build(BuildContext context) {
// Change visuals based on focus/hover state
Color outlineColor = _isFocused ? Colors.black : Colors.transparent;
Color bgColor = _isHovered ? Colors.blue.shade100 : Colors.white;
// Use `GestureDetector` to handle taps
return GestureDetector(
onTap: _handlePressed,
// Focus Actionable Detector
child: FocusableActionDetector(
focusNode: _focusNode,
// Set mouse cursor
mouseCursor: SystemMouseCursors.click,
// Rebuild with hover/focus changes
onShowFocusHighlight: (v) => setState(() => _isFocused = v),
onShowHoverHighlight: (v) => setState(() => _isHovered = v),
// Button Content
child: Container(
padding: const EdgeInsets.all(8),
child: Text(widget.label),
decoration: BoxDecoration(
color: bgColor,
border: Border.all(color: outlineColor, width: 1),
),
),
),
);
复制代码
在上面的代码中,注意你仍然需要包裹一个GestureDetector来检测轻触。此外,注意你如何根据悬停/焦点状态来改变你的ui的外观。
键盘动作
任何控件的最后一步是键盘绑定。默认情况下,大多数操作系统控件支持[Space]和[Enter]来 “提交”。你可以通过在.actions字段中连接内置的ActivateIntent来做到这一点。
FocusableActionDetector(
// Hook up the built-in `ActivateIntent` to submit on [Enter] and [Space]
actions: {
ActivateIntent: CallbackAction<Intent>(onInvoke: (_) => _handlePressed()),
},
child: ...
),
复制代码
注意:你可以创建你自己的自定义动作和意图,但大多数时候,默认的ActivateIntent已经足够了。
你也可以使用.shortcuts参数添加额外的键绑定。这里我们将[Ctrl + X]绑定到同一个ActivateIntent上。现在,我们的控件将在[Enter]、[Space]和[Ctrl + X]时提交!
FocusableActionDetector(
// Add 'Ctrl + X' key to the default [Enter] and [Space]
shortcuts: {
SingleActivator(LogicalKeyboardKey.keyX, control: true): ActivateIntent(),
},
child: ...
),
复制代码
有了这些,你就有了一个完成的控件,它在桌面、网页和手机上都能完全按照你的期望工作
blog.gskinner.com/wp-content/…
Tab键、回车键、空格键、轻击键都能如期工作!
下面是你的新自定义按钮的完整代码。
class MyCustomButton extends StatefulWidget {
const MyCustomButton({Key? key, required this.onPressed, required this.label}) : super(key: key);
final VoidCallback onPressed;
final String label;
@override
State<MyCustomButton> createState() => _MyCustomButtonState();
}
class _MyCustomButtonState extends State<MyCustomButton> {
bool _isHovered = false;
bool _isFocused = false;
FocusNode _focusNode = FocusNode();
@override
Widget build(BuildContext context) {
// Change visuals based on focus/hover state
Color outlineColor = _isFocused ? Colors.black : Colors.transparent;
Color bgColor = _isHovered ? Colors.blue.shade100 : Colors.white;
return GestureDetector(
onTap: _handlePressed,
child: FocusableActionDetector(
actions: {
ActivateIntent: CallbackAction<Intent>(onInvoke: (_) => _handlePressed()),
},
shortcuts: {
SingleActivator(LogicalKeyboardKey.keyX, control: true): ActivateIntent(),
},
child: Container(
padding: const EdgeInsets.all(8),
child: Text(widget.label),
decoration: BoxDecoration(
color: bgColor,
border: Border.all(color: outlineColor, width: 1),
),
),
),
);
}
void _handlePressed() {
_focusNode.requestFocus();
widget.onPressed();
}
}
复制代码
要查看一个运行中的例子,以及一个自定义复选框的例子,请查看这里:gist.github.com/esDotDev/04…
更进一步…
虽然FocusableActionDetector处理了一些模板,但对于像按钮、复选框、开关等常见的用例来说,它仍然留下了很多东西。具体来说。
- 添加GestureDetector
- 添加ActivateIntent映射
- 维护isHovered和isFocused状态
- 管理提交时的焦点请求
在我们看来,所有这些常见的行为都可以用某种构建器来处理。所以我们继续做了一个
它被称为FocusableControlBuilder,在这里有一个软件包:pub.dev/packages/fo…
FocusableControlBuilder支持GestureDetector,管理FocusNode,为键盘支持添加了一个ActivateIntent,并提供了一个构建方法,可以访问control.isFocused和control.isHovered,以便在视图的构建中使用。呵!
对于大多数使用情况,这将消除大部分的模板。按钮、复选框、切换器、滑块等都可以以这个构建器为基础,用不同的手势或状态对其进行扩展。
如果我们转换前面的例子,它看起来像。
class MyCustomButton extends StatelessWidget {
const MyCustomButton(this.label, {Key? key, required this.onPressed}) : super(key: key);
final String label;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return FocusableControlBuilder(
onPressed: onPressed,
builder: (_, FocusableControlState control) {
Color outlineColor = control.isFocused ? Colors.black : Colors.transparent;
Color bgColor = control.isHovered ? Colors.blue.shade100 : Colors.grey.shade200;
return Container(
padding: const EdgeInsets.all(8),
child: Text(label),
decoration: BoxDecoration(
color: bgColor,
border: Border.all(color: outlineColor, width: 1),
),
);
});
}
}
复制代码
不错啊! 我们希望你觉得这个包有用,如果你觉得有用,请在下面告诉我们