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

Flutter Android TV焦点异常:遥控器箭头无法聚焦首个元素

问题解决:TV端DPAD无法聚焦推荐列表首个元素

核心问题分析

  1. RawKeyboardListener抢占焦点:你使用的临时FocusNode()会被TV端焦点遍历系统识别,导致它先于列表首个元素获取焦点,最终焦点落到第二个元素。
  2. build中频繁请求焦点:在itemBuilder里每次构建都调用FocusScope.of(context).requestFocus(focusNode),会触发多次焦点请求,后续元素的请求会覆盖首个元素的焦点。
  3. 焦点节点管理不规范: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 05:13:16