用户输入与无障碍
真正自适应的应用还需处理用户输入方式的差异,并兼顾无障碍辅助功能。
仅适配应用外观不够,还需支持多种用户输入。鼠标与键盘带来触控设备之外的输入类型,如滚轮、右键、悬停交互、Tab 遍历与键盘快捷键。
部分功能在 Material widget 上默认可用。但若创建了自定义 widget,可能需要自行实现。
优秀应用设计的部分功能也有助使用辅助技术的用户。例如,除属于良好应用设计外,Tab 遍历与键盘快捷键等对_使用辅助设备的用户至关重要_。除创建无障碍应用的标准建议外,本页涵盖同时实现自适应_与_无障碍的信息。
自定义 widget 的滚轮
#ScrollView 或 ListView 等滚动 widget 默认支持滚轮,几乎所有可滚动自定义 widget 都基于它们构建,因此同样有效。
若需实现自定义滚动行为,可使用 Listener
widget 自定义 UI 对滚轮的反应。
return Listener(
onPointerSignal: (event) {
if (event is PointerScrollEvent) print(event.scrollDelta.dy);
},
child: ListView(),
);
Tab 遍历与焦点交互
#使用实体键盘的用户预期可用 Tab 键快速导航应用,运动或视觉差异用户往往完全依赖键盘导航。
Tab 交互需考虑两点:焦点如何在 widget 间移动(遍历),以及 widget 获焦时的视觉高亮。
按钮、文本字段等大多数内置 widget 默认支持遍历与高亮。若希望自定义 widget 参与遍历,可用 FocusableActionDetector
创建控件。该 widget 便于在一个 widget 中组合焦点、鼠标输入与快捷键;可创建定义 action 与键绑定的检测器,并提供处理焦点与悬停高亮的回调。
class _BasicActionDetectorState extends State<BasicActionDetector> {
bool _hasFocus = false;
@override
Widget build(BuildContext context) {
return FocusableActionDetector(
onFocusChange: (value) => setState(() => _hasFocus = value),
actions: <Type, Action<Intent>>{
ActivateIntent: CallbackAction<Intent>(
onInvoke: (intent) {
print('Enter or Space was pressed!');
return null;
},
),
},
child: Stack(
clipBehavior: Clip.none,
children: [
const FlutterLogo(size: 100),
// Position focus in the negative margin for a cool effect
if (_hasFocus)
Positioned(
left: -4,
top: -4,
bottom: -4,
right: -4,
child: _roundedBorder(),
),
],
),
);
}
}
控制遍历顺序
#
要更精确控制用户 Tab 时 widget 的聚焦顺序,可用 FocusTraversalGroup
定义 Tab 时应作为一组处理的树片段。
例如,可先 Tab 遍历表单所有字段,再 Tab 到提交按钮:
return Column(
children: [
FocusTraversalGroup(child: MyFormWithMultipleColumnsAndRows()),
SubmitButton(),
],
);
Flutter 有多种内置遍历 widget 与组的方式,默认使用 ReadingOrderTraversalPolicy。该类通常表现良好,也可改用其他预定义 TraversalPolicy
或创建自定义策略。
键盘加速器
#
除 Tab 遍历外,桌面与 Web 用户习惯将各种快捷键绑定到特定操作。无论是 Delete 快速删除还是 Control+N 新建文档,务必考虑用户预期的加速器。键盘是强大的输入工具,尽量榨取其效率,用户会感谢你!
在 Flutter 中实现键盘加速器有多种方式,取决于你的目标。
若单个 widget(如已有焦点节点的 TextField 或 Button)需处理快捷键,可用 KeyboardListener
或 Focus 包裹并监听键盘事件:
@override
Widget build(BuildContext context) {
return Focus(
onKeyEvent: (node, event) {
if (event is KeyDownEvent) {
print(event.logicalKey);
}
return KeyEventResult.ignored;
},
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: const TextField(
decoration: InputDecoration(border: OutlineInputBorder()),
),
),
);
}
}
要对树中较大部分应用一组键盘快捷键,请使用 Shortcuts
widget:
// Define a class for each type of shortcut action you want
class CreateNewItemIntent extends Intent {
const CreateNewItemIntent();
}
Widget build(BuildContext context) {
return Shortcuts(
// Bind intents to key combinations
shortcuts: const <ShortcutActivator, Intent>{
SingleActivator(LogicalKeyboardKey.keyN, control: true):
CreateNewItemIntent(),
},
child: Actions(
// Bind intents to an actual method in your code
actions: <Type, Action<Intent>>{
CreateNewItemIntent: CallbackAction<CreateNewItemIntent>(
onInvoke: (intent) => _createNewItem(),
),
},
// Your sub-tree must be wrapped in a focusNode, so it can take focus.
child: Focus(autofocus: true, child: Container()),
),
);
}
Shortcuts 很有用,因为仅当该 widget 树或其子级有焦点且可见时才触发快捷键。
最后一种选择是全局监听器,可用于始终开启的应用级快捷键,或面板可见时(无论焦点状态)接受快捷键。用 HardwareKeyboard
添加全局监听器很简单:
@override
void initState() {
super.initState();
HardwareKeyboard.instance.addHandler(_handleKey);
}
@override
void dispose() {
HardwareKeyboard.instance.removeHandler(_handleKey);
super.dispose();
}
用全局监听器检查键组合时,可使用 HardwareKeyboard.instance.logicalKeysPressed 集合。例如,以下方法可检查是否按住所提供的任一键:
static bool isKeyDown(Set<LogicalKeyboardKey> keys) {
return keys
.intersection(HardwareKeyboard.instance.logicalKeysPressed)
.isNotEmpty;
}
将两者结合,可在按下 Shift+N 时触发操作:
bool _handleKey(KeyEvent event) {
bool isShiftDown = isKeyDown({
LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.shiftRight,
});
if (isShiftDown && event.logicalKey == LogicalKeyboardKey.keyN) {
_createNewItem();
return true;
}
return false;
}
使用静态监听器时注意:用户正在字段中输入或关联 widget 不可见时,通常需禁用它。与 Shortcuts 或 KeyboardListener 不同,这由你自行管理。绑定 Delete/Backspace 加速器时尤其重要,若子级有用户可能正在输入的
TextField。
自定义 widget 的鼠标进入、离开与悬停
#在桌面上,常通过改变鼠标光标指示悬停内容的功能。例如,悬停按钮时通常为手型光标,悬停文字时为 I 型光标。
Flutter 的 Material 按钮处理标准按钮与文字光标的基本焦点状态。(例外:若将 Material 按钮默认样式的 overlayColor 设为透明。)
为应用中任何自定义按钮或手势检测器实现焦点状态。若更改默认 Material 按钮样式,请测试键盘焦点状态并在需要时自行实现。
在自定义 widget 内更改光标,请使用 MouseRegion:
// Show hand cursor
return MouseRegion(
cursor: SystemMouseCursors.click,
// Request focus when clicked
child: GestureDetector(
onTap: () {
Focus.of(context).requestFocus();
_submit();
},
child: Logo(showBorder: hasFocus),
),
);
MouseRegion 也适用于创建自定义 rollover 与悬停效果:
return MouseRegion(
onEnter: (_) => setState(() => _isMouseOver = true),
onExit: (_) => setState(() => _isMouseOver = false),
onHover: (e) => print(e.localPosition),
child: Container(
height: 500,
color: _isMouseOver ? Colors.blue : Colors.black,
),
);
有关在获焦时为按钮添加轮廓样式的示例,请参阅 Wonderous 应用的按钮代码。应用通过
FocusNode.hasFocus
检查按钮是否获焦并添加轮廓。
视觉密度
#例如,你可能考虑扩大 widget 的「点击区域」以适配触摸屏。
不同输入设备精度不同,需要不同尺寸的点击区域。Flutter 的 VisualDensity 类便于在全应用调整视图密度,例如在触控设备上让按钮更大(更易点击)。
当你为 MaterialApp 更改 VisualDensity 时,支持它的 MaterialComponents 会动画化其密度以匹配。默认情况下,水平和垂直密度均设为 0.0,但你可以将密度设为任意负值或正值。在不同密度之间切换,可轻松调整 UI。
要设置自定义视觉密度,将密度注入 MaterialApp 主题:
double densityAmt = touchMode ? 0.0 : -1.0;
VisualDensity density = VisualDensity(
horizontal: densityAmt,
vertical: densityAmt,
);
return MaterialApp(
theme: ThemeData(visualDensity: density),
home: MainAppScaffold(),
debugShowCheckedModeBanner: false,
);
在自有视图中使用 VisualDensity 时,可查找:
VisualDensity density = Theme.of(context).visualDensity;
容器不仅会自动响应密度变化,变化时还会动画,将自定义与内置 widget 串联,在全应用实现平滑过渡。
如上所示,VisualDensity 无单位,对不同视图含义可不同。以下示例中 1 密度单位等于 6 像素,但完全由你决定。无单位使其相当灵活,在多数场景应能适用。
值得注意的是,Material 通常每个视觉密度单位约 4 逻辑像素。有关受支持 widget 的更多信息,请参阅 VisualDensity
API。有关密度原则的更多信息,请参阅 Material Design 指南。
除非另有说明,本文档之所提及适用于 Flutter 3.44.0 版本。本页面最后更新时间:2026-06-04。查看文档源码 或者 为本页面内容提出建议。