You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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),
                    ),
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

核心逻辑解释

  1. 状态管理:通过_selectedIndex记录当前选中项索引,初始值-1表示未选中任何项
  2. 键盘监听:使用RawKeyboardListener包裹下拉组件,绑定_listFocusNode并设置autofocus: true,确保组件能捕获键盘事件
  3. 按键处理:
    • 上箭头:若已有选中项则索引减1,未选中时选中最后一项
    • 下箭头:若已有选中项则索引加1,未选中时选中第一项
    • 回车:触发选中项的点击逻辑,完成选择并关闭弹窗
  4. 视觉反馈:根据isSelected给ListTile设置不同的tileColor,让用户清晰识别当前选中项

内容的提问来源于stack exchange,提问作者chethan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 09:07:47