as

Settings
Sign out
Notifications
Alexa
亚马逊应用商店
AWS
文档
Support
Contact Us
My Cases
新手入门
设计和开发
应用发布
参考
支持

焦点管理

焦点管理

焦点管理让用户可在您的应用中选择特定的视觉元素或组件。在物理设备上,这通过方向键上的按键输入来完成。在模拟器上进行测试时,使用箭头键 + 回车键。

启用焦点管理才能让用户轻松进行电视导航。没有视觉反馈,就无法让用户知道图像或按钮等视觉元素是否需要进一步操作。要为电视平台构建应用,您需要一种方法来处理应用中用户界面元素的焦点并提供反馈。

焦点管理概述视频

以下视频介绍了如何在Vega Fire TV设备上进行焦点管理。(附带中文字幕)

Vega的基本焦点处理

如何表示焦点

焦点指示器是重要的视觉指示,用于显示屏幕上的哪个元素当前具有键盘焦点。虽然这些指示器通常通过背景颜色或不透明度调整等样式更改来实现,但出于无障碍功能的原因,加入物理属性更改至关重要。

物理更改(例如添加边框、修改元素大小或引入其他用户界面组件)可确保焦点状态在各种条件下保持可见。与纯粹的视觉更改(颜色或不透明度)不同,即使启用了高对比度模式等无障碍功能设置,仍然可以检测到物理变化。

聚焦时带有即开即用指示的组件

在Vega中,与React Native类似,大多数组件在平台级别不提供焦点指示。只有两个组件(<Button/><TouchableOpacity/>)在聚焦时会改变其外观。

  未聚焦(默认状态) 聚焦(不透明度已更改)
<Button/>
<TouchableOpacity/> (不透明度已更改为0.2,可以使用activeOpacity属性进行修改。)

在Vega中对焦点变化做出回应

您可以使用onFocusonBlur属性,根据组件的焦点状态动态更改组件的外观。这些属性接受回调函数,当组件获得或失去焦点时,会分别触发对应的回调函数。

以下是组件获得焦点时为其添加蓝色边框的示例:

已复制到剪贴板。


const FocusablePressable = () => {
  const [isFocused, setIsFocused] = useState(false);  // 使用状态来追踪焦点。

  return (
    <Pressable
      style={[styles.button, isFocused && styles.buttonFocused]}  // 根据焦点状态有条件地提供样式。
      onFocus={() => setIsFocused(true)} // 通过onFocus() 设置焦点状态。
      onBlur={() => setIsFocused(false)} // 通过onBlur() 取消焦点状态。
    >
      <Text style={[styles.text]}>
        {'Presssable'}
      </Text>
    </Pressable>
  );
};

const styles = StyleSheet.create({
  button: {
    padding: 10,
    backgroundColor: 'gray',  // 提供默认边框颜色。
    borderRadius: 5,
  },
  buttonFocused: {
    borderWidth: 2,
    borderColor: 'blue', // 当获得焦点时,将边框颜色改为蓝色。

  },
});

Vega中组件的可设定焦点功能

在电视应用中使用交互式组件时,必须了解不同组件上的焦点行为的运作方式。某些组件默认处于聚焦状态,而另一些组件可以使用focusable属性设置为可聚焦或不可聚焦。使用诸如FlatListTVFocusGuideView之类的容器时,焦点行为可能会根据容器及其子组件的可设定焦点设置而变化。

Vega中的焦点导航行为

此部分探讨Vega中的焦点导航行为,使用示例来演示各种场景和预期行为。Vega旨在提供与Fire OS上的React Native一致的体验。

笛卡尔焦点管理

笛卡尔焦点管理(或隐式焦点管理)是一种策略,在这种策略中,焦点沿着方向键按下方向移至“最接近”的项目。在模拟器上,您使用的是箭头键 + 回车键。除非您的应用实现了特定的覆盖逻辑,否则Vega会应用笛卡尔焦点管理(请参阅Vega中的自定义焦点行为部分,了解覆盖焦点行为的不同方法)

在沿水平和垂直轴排列形状均匀的项目的布局中,焦点导航比较直观:

  • 按下向下按钮时,会将焦点移至正下方的项目。
  • 按下向右按钮时,会将焦点水平移动到相邻的项目。

在包含形状不均匀的项目的布局中,当用户使用遥控器上的方向键选择移动方向时,任何与方向键控制事件处于相同大方向的元素都是获得焦点的候选元素。处于最短加权距离的候选项(沿输入方向的距离加权幅度较小)被视为下一个聚焦候选项。

选择焦点候选元素时,如果存在组件重叠,并不会考虑视图层次结构中的Z顺序如何。例如,当您按下向上按钮时,如果有一个组件被部分遮挡,但计算出它位于较短的加权距离,则会将焦点置于其上,而不是更远的未被遮挡的组件。以下部分提供了有关组件在不同布局中如何获得焦点的详细案例研究。

不同布局中笛卡尔焦点导航算法的案例研究

在处理更复杂的布局时,尤其是形状不均匀的项目时,焦点导航将依赖于组件的大小和空间关系。尽管Vega实现了类似于Fire OS的聚焦算法以保持一致性,但某些场景可能会根据项目的大小和位置产生不同的导航行为。

以下是一些直接投影在垂直轴路径上的项目无法获得焦点的案例研究。

案例研究1: 调查如下案例:按下向下按钮时,焦点未移至正下方项目

该设置包含4个项目,并将模拟如下用例:焦点置于 [顶部项目] 上,然后用户按下向下按钮。

该设置包含4个项目,并将模拟如下用例:焦点置于 [顶部项目] 上,然后用户按下向下按钮。

已复制到剪贴板。

[顶部项目] [顶部项目旁边]
      |
      v
            [侧面项目]
[底部项目]

演示: 当 [侧面项目] 与 [底部项目] 水平重叠时

左侧演示的行为:

  • 从 [顶部项目] 按下向下按钮:
    • 焦点移至 [较小项目]。
  • 发生这种情况是因为 [较小项目] 的底部边缘和 [底部项目] 的顶部边缘没有水平重叠。

右侧演示的行为:

  • 使用相似的组件定位,焦点会改为移至 [底部项目]。
  • 之所以会出现这种不同的行为,是因为 [较小项目] 的底部边缘现在与 [底部项目] 的顶部边缘水平重叠。

演示: 当 [侧边项目] 与 [底部项目] 垂直重叠时

顶部演示的行为:

  • 从 [顶部项目] 按下向下按钮:
    • 焦点移至 [较小项目]。
  • 发生这种情况是因为 [较小项目] 的左边缘与 [顶部项目]] 的右边缘垂直重叠。

底部演示的行为:

  • 使用相似的组件定位: 焦点改为移至 [底部项目]。
  • 发生这种情况是因为 [较小项目] 的左边缘与 [顶部项目]] 的右边缘没有垂直重叠。

摘要

焦点算法考虑的不仅仅是对齐。

  • 组件之间的边缘重叠很重要。
  • 考虑了与下一个可设定焦点元素的距离。
  • 组件定位严重影响导航路径。

可视化有助于理解为什么有时焦点的移动可能看起来出乎意料,以及如何为所需的导航路径定位组件。

覆盖行为的建议

如果不需要默认焦点导航的行为,Vega提供不同的API来覆盖该行为:

  • FocusManager.setNextFocus() 方法。
  • nextFocusUp/nextFocusDown/nextFocusLeft/nextFocusRight属性。
  • FocusManager.focus() 以编程方式将焦点分配给特定元素。
  • <TVFocusGuideView>中的destination属性控制导航到容器中时的焦点。

案例研究2: 在不同宽度的项目行之间导航时,焦点会跳过全宽元素

在以下组件布局中,当焦点位于项目1上且用户按下向下按钮时,焦点可能会转移到 [项目1] 下方的 [全宽项目] 或移动到 [全宽项目],具体取决于组件的布局。

已复制到剪贴板。

[ 项目 1]
[      全角项目         ]
[ 项目 3 ] [ 项目 4 ] [ 项目 5 ]

演示应用解释了可能影响焦点导航的标准。

基准场景

假设 [全角项目] 的基准尺寸的宽度为1208,高度为104。使用此配置,按下向下按钮时,焦点从 [项目1] 移至 [项目3]。

从项目1向下导航时,会跳过全宽项目2。

[全宽] 项目的高度会影响导航的行为模式的演示。

场景1: 减小宽度可防止焦点跳过更宽的项目

当宽度从1208减小到1070且高度保持不变时,从 [项目1] 按下向下按钮时,[全宽项目] 会获得焦点。

将宽度从1208更改为1070后,从项目1向下导航时,全宽项目2获得焦点。

场景2: 增加高度可防止焦点跳过更宽的项目

通过将 [全宽项目] 的高度从104增加到105(宽度保持不变),从 [项目1] 按下向下按钮时,[全宽项目] 获得焦点。

将高度从104更改为105后,从项目1向下导航时,全宽项目2获得焦点。

覆盖默认行为的实现

默认的焦点行为可能无法满足所有应用的需求。相关应用可以使用FocusManager Turbo模块中的setNextFocus(),声明式地定义组件之间的自定义焦点移动路径。

  • 按下向下按钮后,强制焦点从 [项目1] 移至 [全宽项目]。
  • 从 [项目3] 向上导航到 [全角项目]。
  • 从 [全角项目] 向下导航到 [项目3]。

Vega中的焦点行为

管理焦点在不同行的项目之间的切换

ScrollViewFlatList等列表具有基本的焦点管理支持。使用了基于索引的导航,并且仅当下一个可设定焦点的项目不在屏幕上或在ScrollView边界之外时,才会滚动列表。从最后一个元素向右或者从第一个元素向左滚动时,焦点会移动到下一个或上一个列表。例如,当焦点位于列表右端的最后一个元素上时,按下向右键会将焦点移动到下方列表中的对应磁贴上。如果焦点在元素1.3上,则按下方向键向右按钮会将焦点移至元素2.3。

上面示例中的默认焦点行为可能不是所需的用户体验。有些应用可能更愿意将焦点“锁定”在行尾。在上面的示例中,如果焦点在元素1.3上,则按方向键向右按钮不会移动焦点。FlashListFlatList都有建议的实现。

恢复焦点

平台限制

Vega平台不提供内置的焦点恢复功能。通过特定React Navigation堆栈导航器可以恢复焦点。

堆栈导航器行为

  • @amazon-devices/react-navigation__stack
    • ❌ 没有自动焦点恢复
  • @amazon-devices/react-navigation__native-stack
    • ❌ 没有自动焦点恢复

恢复焦点的实施选项

对于使用React Navigation构建的多屏应用:

  • 对每个屏幕都存储一个最近聚焦元素的引用。
  • 使用useFocusEffect() 来检测用户何时返回屏幕。
  • useFocusEffect() 中,使用保存的引用调用FocusManager.setFocus(),将焦点恢复到正确的元素上。

配置初始焦点

管理初始焦点主要有两种方法。

  • hasTVPreferredFocus属性会在组件初始挂载期间设置焦点。此属性仅在初始渲染阶段有效。
  • 您可以将useEffectuseFocusEffect来自React Navigation)与FocusManager.setFocus() 结合使用,以获得更具动态性的方法,尤其是当您的应用需要在使用React Navigation构建的多页应用中主动恢复焦点状态时。这种方法为初始挂载阶段之后的焦点行为提供了更大的灵活性和控制度。

自定义焦点行为

Vega为焦点管理提供了以下范例。

  • 焦点属性和方法: 如果您想覆盖应用中特定组件的默认聚焦行为,Vega支持各种与焦点相关的属性和TurboModule方法。可以把它们的用途视为定义您不需要默认行为时的边缘情况。
  • 专用焦点组件: 在某些情况下,您可能会希望行为以某种系统化的方式与默认笛卡尔行为有所不同(例如,您可能熟悉TVFocusGuideView)。该部分包含Vega支持的自定义组件(包括TVFocusGuideView)的列表。

焦点属性和方法

焦点属性是您可以在组件上设置的属性(将focusable设置为true),以改变焦点以这些组件为起点进行移动的方式。以下是每个方向的焦点属性清单。

  • nextFocusUp
  • nextFocusDown
  • nextFocusLeft
  • nextFocusRight

要在任何方向上覆盖组件的下一个焦点值,可设置nextFocusUpnextFocusDownnextFocusLeftnextFocusRight属性。将值设置为同一组件的节点句柄,可以有效地阻止在给定方向上按下按钮的行为。

  • 将该值设置为undefined可重新启用沿给定方向按下按钮的默认笛卡尔行为
  • 如果您想要一个更强制性的API,FocusManager TurboModule提供的函数可以反映上述焦点属性的行为。还有一个setFocusRoot API可以阻止焦点离开组件或其子项。有关更多详细信息,请参阅焦点管理器 (Vega)

示例:

已复制到剪贴板。

const useNodeHandle = (ref) => {
  const [nodeHandle, setNodeHandle] = React.useState<number | null>(null);
  React.useEffect(() => {
    if (ref.current) {
      setNodeHandle(findNodeHandle(ref.current));
    }
  }, [ref.current]);
  return nodeHandle;
};

const focusableComponent = () => {
  const ref1 = React.useRef(null);
  const ref2 = React.useRef(null);
  const ref3 = React.useRef(null);

  const ref1Handle = useNodeHandle(ref1);
  const ref2Handle = useNodeHandle(ref2);
  const ref3Handle = useNodeHandle(ref3);

  // 三个可设定焦点组件显式定义了它们的部分或全部
  // 方向覆盖。将下一个焦点设置至同一组件的覆盖
  // 有效地发挥作用,阻止了按钮的按下。对于
  // 方向未明确声明或设置为未定义的组件,
  // 将采用笛卡尔坐标聚焦策略的回退行为。
  return (
          <View>
            <TouchableOpacity
                    ref={ref1}
                    onPress={() => {}}
                    // 阻止按下“向上”
                    nextFocusUp={ref1Handle || undefined}
                    // 阻止按下“向右”
                    nextFocusRight={ref1Handle || undefined}
                    // 在按下“向左”时显式聚焦于组件2
                    nextFocusLeft={ref2Handle || undefined}
                    // 在按下“向下”时显式聚焦于组件3
                    nextFocusDown={ref3Handle || undefined}>Component 1</TouchableOpacity>
            <TouchableOpacity
                    ref={ref2}
                    onPress={() => {}}
                    // 在按下“向右”时显式聚焦于组件1
                    nextFocusRight={ref1Handle || undefined}>Component 2</TouchableOpacity>
            <TouchableOpacity
                    ref={ref3}
                    onPress={() => {}}
                    // 在按下“向上”时显式聚焦于组件1
                    nextFocusUp={ref1Handle || undefined}>Component 3</TouchableOpacity>
          </View>
  );
};

FocusManager Turbo模块

除了上述属性外,FocusManager Turbo模块还为定义焦点行为边缘情况提供了一个命令式API。有关支持的方法的列表,请参阅焦点管理器 (Vega)

  • setNextFocus()
  • clearNextFocus()
  • setFocusRoot()
  • focus()
  • blur()
  • getFocused()

专用焦点组件

TVFocusGuideView

TVFocusGuideView提供React Native的TVFocusGuide API的Vega实现。有关更多信息,请参阅TVFocusGuideView。该组件是从react-native-tvos的移植。

调度同步焦点事件

这是Vega提供的一项新功能,允许在用户界面线程和JS之间同步调度焦点事件。用户界面线程会等待处理新的焦点变化,直到之前的焦点或模糊事件完成执行。在用户界面线程被阻止时收到的任何其他焦点或模糊事件都将排入队列。

同步焦点旨在解决在快速按键时出现的焦点问题:

  • 当应用在获得焦点后更新项目的zIndex时,zIndex的更改会导致该项目被移除并重新插入。因此,不能再聚焦项目,并且丢失焦点。在另一种情况下,如果将项目放在 <TVFocusGuideView> 内,不会失去焦点,而是会由TVFocusGuide将焦点恢复到第一个子项身上。

在考虑使用该功能之前,请查看以下部分,了解有关副作用和对用户体验的潜在影响的详细信息。

您的应用是否应该启用同步焦点

在以下情况下可考虑使用同步焦点:

  • 您会看到将焦点重置回<TVFocusGuideVIew/>中的第一个项目的问题。
  • 有一项特定的用户界面要求,即严格执行在焦点移至另一项目之前已完成的项目聚焦或模糊化的所有副作用。

我们的指导如下:

  • 尽可能减少使用量。
  • 您的应用需要进行优化并遵循应用性能最佳实践,以避免不必要的重新渲染。
  • 尽可能减轻onFocus()onBlur() 的负载,以避免阻塞用户界面线程的时间过长。

由于焦点和模糊事件是同步调度的,这意味着在任何onFocus()onBlur() 事件返回之前,用户界面线程会被阻止并处于空闲状态。在快速按键期间,这会对性能和渲染产生不同的影响。下一部分详细介绍一些已知的影响和解决方案。

已知的影响和解决方案

快速按键时可能会跳过较长的动画

如果应用在聚焦或模糊项目时安排了动画,则当持续时间长于每次按键之间的间隔时,可能会跳过动画。如果在应用中观察到该行为,请缩短动画持续时间以提高响应能力。

当有不必要的重新渲染时,应用中会出现延迟行为

同步调度事件时,需要对应用进行优化,以避免不必要的重新渲染。有关优化的更多信息,请参阅应用性能最佳实践

此外,为避免阻塞用户界面线程的时间过长,应用不应有任何长时间运行的逻辑作为onFocus()onBlur() 回调的一部分。

调度焦点或模糊事件时,在进行中的动画中可能会出现掉帧现象

在JavaScript onFocus()onBlur() 回调返回之前,用户界面线程会在后台被阻止。如果应用具有重复出现的动画,或具有在调度事件的同时运行的任何动画,可能会观察到掉帧现象。

如何启用同步焦点事件

适用于Vega的React Native实现了enableSynchronousFocusEventsVega属性。此属性允许应用有选择地在每个组件的同步和异步焦点事件调度之间进行选择。

如何使用属性:

  • enableSynchronousFocusEventsVega设置为true时。该应用不应在onFocus()onBlur() 实现内调用任何可能以编程方式改变焦点的方法。这些方法包括:

    • requestTVFocus()
    • FocusManager.Focus().Blur()
    • ref.focus() and ref.blur()
  • 初始渲染后不要更改enableSynchronousFocusEventsVega的值。

  • 我们不建议在共用父容器下混用具有不同enableSynchronousFocusEventsVega值的组件。

    例如,避免以下混用组件的enableSynchronousFocusEventsVega的模式:

已复制到剪贴板。

    <View>
      <Pressable enableSynchronousFocusEventsVega>
      <Pressable >
      <Pressable enableSynchronousFocusEventsVega>
    <View>
    ```
</div>

  应改为使用容器中所有组件都具有相同值的模式:
<p><button id="uMDWx18h_copy-button" type="button" class="btn btn-default btn-sm copy-button" data-clipboard-action="copy">
    <i class="fa fa-files-o" aria-hidden="true"></i> Copy code </button>
    <span id="uMDWx18h_copy-button_tooltip" class="tooltip-for-copy-button">Copied to clipboard.</span></p><script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js"></script><script src="https://amzndevresources.com/jekyll/js/clipboardcopy.js"></script>

<div id="uMDWx18h" markdown="block">

```tsx
    <PageContainer>
      <Navbar>
        <TouchableOpacity enableSynchronousFocusEventsVega>
        <TouchableOpacity enableSynchronousFocusEventsVega>
      </Navbar>
      <HomeContent>
        <Pressable >
        <Pressable >
      </HomeContent>
    </PageContainer>
    ```
</div>

用法示例:
<p><button id="mXkzrypn_copy-button" type="button" class="btn btn-default btn-sm copy-button" data-clipboard-action="copy">
    <i class="fa fa-files-o" aria-hidden="true"></i> 复制代码 </button>
    <span id="mXkzrypn_copy-button_tooltip" class="tooltip-for-copy-button">Copied to clipboard.</span></p><script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js"></script><script src="https://amzndevresources.com/jekyll/js/clipboardcopy.js"></script>

<div id="mXkzrypn" markdown="block">

```tsx
<View
   focusable
   enableSynchronousFocusEventsVega
   onFocus={() => { console.log('view-onfocus'); }}
   onBlur={()=> { console.log('view-onBlur');}}
>
   <Text>
       {`用于使接收了onBlur/onFocus的View同步的示例`}
   </Text>
</View>

<TouchableOpacity
   enableSynchronousFocusEventsVega
   onPress={() => { console.log("touchableOpacity-pressed"); }}
   onFocus={() => { console.log('touchableOpacity-onfocus'); }}
   onBlur={()=> { console.log('touchableOpacity-onBlur'); }}
>
   <Text>
       {`用于使接收了onBlur/onFocus的TouchableOpacity同步的示例`}
   </Text>
</TouchableOpacity>

react-native-tvos、react-native-tvos和react-native-kepler

Vega平台使用react-native-kepler Javascript程序包来支持React Native。此举遵循的方法与其他树外平台(例如react-native-macos)类似。

一般建议从react-native命名空间导入API和组件,因为这可以为应用开发者提供跨React Native支持的所有平台的可移植性。react-native允许使用Metro配置共享相同的命名空间,详细信息请见React Native文档。

在处理特定于平台的功能时,这种方法会变得更加复杂。例如,FocusManager等特定于平台的API只在react-native-kepler命名空间中提供,而从react-native中导入此类API会导致错误。

Vega平台同时支持多模态和电视平台,这会使情况变得更加复杂。React Native采用了分叉方法,通过创建react-native-tvos来添加对电视平台的支持和TVFocusGuideView等功能。以上内容均通过react-native-tvos命名空间提供。

相比之下,Vega已将电视特定组件(例如TVFocusGuideView)移植到react-native-kepler命名空间中。因此,该单一命名空间即可同时支持多模态和电视平台。您可以从@amazon-devices/react-native-kepler命名空间导入电视特定组件和API。


Last updated: 2025年10月1日