Flutter Android TV焦点异常:遥控器箭头无法聚焦首个元素
问题解决:TV端DPAD无法聚焦推荐列表首个元素
核心问题分析
- RawKeyboardListener抢占焦点:你使用的临时
FocusNode()会被TV端焦点遍历系统识别,导致它先于列表首个元素获取焦点,最终焦点落到第二个元素。 - build中频繁请求焦点:在
itemBuilder里每次构建都调用FocusScope.of(context).requestFocus(focusNode),会触发多次焦点请求,后续元素的请求会覆盖首个元素的焦点。 - 焦点节点管理不规范:
lastItemFocusNode命名混淆(实际用于首个元素),且非首个元素的焦点节点未正确管理,存在内存泄漏风险。
修复步骤
1. 规范焦点节点管理
在你的State类中初始化并管理焦点节点:
FocusNode _firstItemFocusNode = FocusNode(); List<FocusNode> _itemFocusNodes = []; @override void initState() { super.initState(); } @override void dispose() { _firstItemFocusNode.dispose(); for (var node in _itemFocusNodes) { node.dispose(); } super.dispose(); }
2. 修改RawKeyboardListener配置
给它的焦点节点设置skipTraversal: true,避免参与焦点遍历:
if (_showSlider) RawKeyboardListener( focusNode: FocusNode(skipTraversal: true), // 跳过焦点遍历 onKey: (RawKeyEvent event) { print('object'); _resetSliderTimer(); }, child: Align( alignment: Alignment.bottomCenter, child: SizedBox( width: MediaQuery.of(context).size.width, child: landscape(videoDetailsProvider.sectionDetailModel.getRelatedVideo), ), ), ),
3. 重构landscape函数
- 移除
itemBuilder内的焦点请求逻辑 - 在列表构建完成后,通过
addPostFrameCallback统一请求首个元素焦点 - 规范焦点节点分配:
Widget landscape(List<GetRelatedVideo>? relatedDataList) { if (relatedDataList == null || relatedDataList.isEmpty) { return Placeholder(); } // 初始化/更新焦点节点列表 if (_itemFocusNodes.length != relatedDataList.length) { _itemFocusNodes.clear(); for (int i = 0; i < relatedDataList.length; i++) { _itemFocusNodes.add(i == 0 ? _firstItemFocusNode : FocusNode()); } } // 列表构建完成后请求首个元素焦点 WidgetsBinding.instance.addPostFrameCallback((_) { if (_showSlider) { FocusScope.of(context).requestFocus(_firstItemFocusNode); } }); return Container( width: MediaQuery.of(context).size.width, height: Dimens.heightLand, margin: const EdgeInsets.only(top: 12), child: ListView.separated( itemCount: relatedDataList.length, shrinkWrap: true, padding: const EdgeInsets.fromLTRB(12, 0, 0, 0), scrollDirection: Axis.horizontal, separatorBuilder: (context, index) => const SizedBox(width: 0), itemBuilder: (BuildContext context, int index) { final focusNode = _itemFocusNodes[index]; return Focus( focusNode: focusNode, onFocusChange: (isFocused) { // 可选:聚焦时自动滚动到可视区域 if (isFocused) { final listView = context.findAncestorWidgetOfExactType<ListView>(); listView?.controller?.jumpTo(index * Dimens.widthLand); } }, child: FocusBase( onFocus: (isFocused) {}, focusColor: white, onPressed: () async { log("Clicked on index ==> $index"); openDetailPage( index, relatedDataList[index].id ?? 0, relatedDataList[index].upcomingType ?? 0, relatedDataList[index].videoType ?? 0, relatedDataList[index].typeId ?? 0, ); }, child: Container( width: Dimens.widthLand, height: Dimens.heightLand, alignment: Alignment.center, child: ClipRRect( borderRadius: BorderRadius.circular(4), clipBehavior: Clip.antiAliasWithSaveLayer, child: MyNetworkImage( imageUrl: relatedDataList[index].landscape.toString() ?? "", fit: BoxFit.cover, imgHeight: MediaQuery.of(context).size.height, imgWidth: MediaQuery.of(context).size.width, ), ), ), ), ); }, ), ); }
4. 移除build中的无效焦点请求
删掉原build方法里被注释的FocusScope.of(context).requestFocus(lastItemFocusNode);,避免干扰。
额外说明
TV端焦点机制和PC端差异较大:PC端Tab是线性遍历,而TV端DPAD是基于焦点树的方向遍历。确保所有不需要参与焦点遍历的节点(如RawKeyboardListener)都设置skipTraversal: true,同时避免在build周期内频繁触发焦点请求,否则会导致焦点漂移。
内容的提问来源于stack exchange,提问作者Md.Nayeem Ahmed 201-15-3302
相关产品推荐
相关产品推荐

