Flutter Web实现ListTile上下箭头键盘导航无障碍访问方案
Flutter Web 实现ListTile键盘上下箭头切换选中功能
要实现上下箭头切换ListTile选中并支持回车确认,需要在现有代码基础上添加状态管理、键盘事件监听和视觉反馈,以下是完整解决方案:
关键改动说明
- 用
StatefulWidget维护当前选中项的索引状态 - 通过
RawKeyboardListener捕获键盘上下箭头及回车事件 - 处理索引边界,避免越界
- 给选中项添加明显的视觉样式区分
修改后的完整代码
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; // 假设你的location数据模型 class LocationItem { final String title; final String subtitle; LocationItem({required this.title, required this.subtitle}); } class LocationDropdown extends StatefulWidget { final TextEditingController controller; final VoidCallback hideOverlay; final FocusNode focusNode; final List<LocationItem> location; final List<bool> hoveredindex; const LocationDropdown({ super.key, required this.controller, required this.hideOverlay, required this.focusNode, required this.location, required this.hoveredindex, }); @override State<LocationDropdown> createState() => _LocationDropdownState(); } class _LocationDropdownState extends State<LocationDropdown> { int _selectedIndex = -1; // 初始未选中任何项 final FocusNode _listFocusNode = FocusNode(); // 用于监听键盘事件的焦点节点 @override void dispose() { _listFocusNode.dispose(); super.dispose(); } void _handleKeyEvent(RawKeyEvent event) { if (event is RawKeyDownEvent) { // 处理上箭头 if (event.logicalKey == LogicalKeyboardKey.arrowUp) { setState(() { if (_selectedIndex > 0) { _selectedIndex--; } else if (_selectedIndex == -1 && widget.location.isNotEmpty) { // 未选中时按上箭头,选中最后一项 _selectedIndex = widget.location.length - 1; } }); } // 处理下箭头 else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { setState(() { if (_selectedIndex < widget.location.length - 1) { _selectedIndex++; } else if (_selectedIndex == -1 && widget.location.isNotEmpty) { // 未选中时按下箭头,选中第一项 _selectedIndex = 0; } }); } // 处理回车选中 else if (event.logicalKey == LogicalKeyboardKey.enter && _selectedIndex != -1) { _selectItem(_selectedIndex); } } } void _selectItem(int index) { widget.controller.text = widget.location[index].title; widget.hideOverlay(); widget.focusNode.unfocus(); setState(() { widget.hoveredindex[index] = false; _selectedIndex = -1; }); } @override Widget build(BuildContext context) { return Material( type: MaterialType.transparency, elevation: 5, child: RawKeyboardListener( focusNode: _listFocusNode, onKey: _handleKeyEvent, autofocus: true, // 自动获取焦点以接收键盘事件 child: Container( color: Colors.amber.shade100, width: 288, child: Column( children: [ Container( padding: const EdgeInsets.only(left: 16, right: 8), width: 288, height: 56, decoration: const BoxDecoration( border: Border.all(color: Colors.grey, width: 3), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( width: 219, height: 56, child: TextFormField( controller: widget.controller, decoration: const InputDecoration( border: InputBorder.none, hintText: "Going to?", ), textAlignVertical: TextAlignVertical.top, ), ), InkWell( child: Container( width: 20, decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(20.0)), border: Border.all(color: Colors.grey, width: 2), ), child: const CircleAvatar( radius: 10, backgroundColor: Colors.transparent, child: Icon(Icons.close, size: 20), ), ), onTap: widget.hideOverlay, ) ], ), ), ListView.builder( itemCount: widget.location.length, shrinkWrap: true, itemBuilder: (context, index) { final isSelected = _selectedIndex == index; return Container( decoration: BoxDecoration( color: Colors.amber.shade50, ), child: ListTile( tileColor: isSelected ? Colors.blue.shade100 : Colors.grey, // 选中项样式区分 title: Text(widget.location[index].title), subtitle: Text(widget.location[index].subtitle), onTap: () => _selectItem(index), ), ); }, ), ], ), ), ), ); } }
核心逻辑解释
- 状态管理:通过
_selectedIndex记录当前选中项索引,初始值-1表示未选中任何项 - 键盘监听:使用
RawKeyboardListener包裹下拉组件,绑定_listFocusNode并设置autofocus: true,确保组件能捕获键盘事件 - 按键处理:
- 上箭头:若已有选中项则索引减1,未选中时选中最后一项
- 下箭头:若已有选中项则索引加1,未选中时选中第一项
- 回车:触发选中项的点击逻辑,完成选择并关闭弹窗
- 视觉反馈:根据
isSelected给ListTile设置不同的tileColor,让用户清晰识别当前选中项
内容的提问来源于stack exchange,提问作者chethan
相关产品推荐
相关产品推荐

