TV remote interactions

Focus Manager on web-based platforms (Samsung & LG)

Cell states:

Common cells are available in 2 states:

  • focused
  • unfocused

Menu cells are available in 4 states:

  • unfocused and unselected
  • focused and unselected
  • unfocused and selected
  • focused and selected

In the following diagrams, the components will be marked in blue, and the potential targets of focus movement will be marked in green. See table below.

Normal statesTarget states
default.pngtargets.png

Directions:

The direction of potential targets areas is calculated from the edge of the component holding the "focused" state. There are 4 directions:

  • left
  • up
  • right
  • down
VerticalHorizontal
UpRight
targets.pngdefault.png
DownLeft
default.pngtargets.png

Tree structure:

All focusable items are kept in a logical tree which makes focus movement between items easier and quicker. Focusable, as well as FocsuableGroups items, are wrapped in nodes. The node contains the following information:

  • id - for root component, it is root.
  • component- Focusable or FocusableGroupor null for root.
  • parent- parent node
  • children- array of nodes inside of group
  • lastFocusedItem if this is a group node it might hold id of last selected item in group.

Two nodes inside of the same group will be called siblings. Diagram below shows how components and siblings are structured:

Components in treeSiblings in tree
default.pngtargets.png

Targets:

Targets are siblings of the currently focused component or siblings of the parent in a specified direction. If there are no targets in the current group, the algorithm goes up the tree until it finds a sibling. If a sibling has been found the algorithm will try to find the most accurate focusable item based on distance and priorities.

Siblings found in group:

in-group.png

Siblings found for parent group

moving-groups.png

moving-groups-up.png

No siblings at any level

no-siblings.png

Focus priorities:

  1. Currently selected item - like a menu item
  2. Previously focused item in the parent group
  3. Preferrable items - items which have been marked as preferable
  4. The first item on the list - if by any accident above priories are not satisfied

Focusable component:

In code, the component which supposes to be focusable will have to be wrapped with Focusable component from package react-native-ui-components and can provide optional selected state. Each focusable component has to have defined unique id as well as it needs to provide groupId. As a developer, you can define your preferable focus by adding preferredFocus. The focused state will be provided as below:

<Focusable
id={id}
groupId={"menu"}
preferredFocus={index === 0} //optional
selected={isSelected}
>
//optional
{(focused) => <YourComponent />}
</Focusable>

FocusableGroup component:

Each Focusable item has to be placed in FocusableGroup. FocusableGroup can containt either FocusableGroup or Focusable, however all children need to be the same.

<FocusableGroup
id={id}
groupId={"xyz"} //optional
preferredFocus={index === 0}
>
//optional
<Focusable />
<Focusable />
<Focusable />
</FocusableGroup>

Focus events sequence:

Focus events are propagated to two Focusable items, current and next, when moving focus in following sequence:

  • current.willLoseFocus
  • if next has been found
    • next.willReceiveFocus
    • current.blur
    • current.hasLostFocus
    • next.focus
    • next.hasReceivedFocus
  • else
    • current.failedLostFocus

Focus Manager on Android TV

This document aims to provide information regarding the new focus manager released for the Android TV platform.

Why creating a new focus manager for Android TV ?

Initially, Android TV and the web based platforms both relied on the same idea for the focus manager: measuring where items are, and deciding what next to focus based on the structure of the content, and the physical placement of the focusable items on the screen.

On Android TV, this turned out to cause a major performance hit. So we decided to refactor the focus manager on Android TV, to improve performances, and provide a more effective API.

How does it work

The new focus manager on Android TV relies on a simple idea: each Focusable item needs to provide the reference of the React node to focus next, based on the direction of the navigation.

function MyComponent(props) {
const button1Ref = React.useRef(null);
const button2Ref = React.useRef(null);
return (
<View style={{ flex: 1, flexDirection: "column"}}>
<Focusable id="button-1" ref={button1Ref} nextFocusDown={button2Ref} />
<Focusable id="button-2" ef={button2Ref} nextFocusUp={button1Ref}>
<View>
)
}

This way, each plugin developer can very easily and very effectively state which focus needs to be assigned when the user is pressing on the D-pad buttons, or pressing on them.

The Focusable component doesn't render anything. it simply holds the ability to be focused. You need to declare in its children prop what needs to be render. This takes a function which passes a boolean indicating the focused state, which could be used for styling for instance.

return (
<Focusable id="focusable" ref={ref} nextFocusDown={upRef}>
{focused => <View style={{ backgroundColor: focused ? "red" : "grey" }}>}
<Focusable>
)

This child function also provides a parentFocus object which can be used to delegate the attribution to the next focus in a given direction to what the parent Focusable is declaring. Indeed, Focusable can be nested. Imagine a screen where you have a navbar, and a main section with 3 buttons laid out on top of one another. You want the focus to go down from the navbar to the main section and its top button, and up from the top button to the navbar:

function MyScreen(props) {
const navRef = React.useRef(null);
const mainRef = React.useRef(null);
const buttonRefs = React.useRef([]);
return (
<View>
<Focusable id="nav" ref={navRef} nextFocusDown={mainRef}>
{ (focused, parentFocus) => <NavBar {...{ focused, parentFocus} } />}
</Focusable>
<Focusable id="main" ref={mainRef} nextFocusUp={navRef}>
{ (focused, parentFocus) => buttons.map((button, index) => (
<Focusable
id={button.id}
ref={buttonRefs[index]}
nextFocusUp={ buttonRefs?.[index - 1] || parentFocus.nextFocusUp}
nextFocusDown={ buttonRefs?.[index + 1] }
>
{ (focused) => <Button state={focused} >}
</Focusable>
))}
</Focusable>
</View>
)
}

In reality, your children node are likely to be in another React component. be sure to pass this parentFocus prop to allow navigation to work outside of your screen or component. You will also need to assign focus to the focusable components, when your parent is acquiring focus. You can easily do this by using our useInitialFocus hook

function MyScreen(props) {
const navRef = React.useRef(null);
const mainRef = React.useRef(null);
return (
<View>
<Focusable id="nav" ref={navRef} nextFocusDown={mainRef}>
{(focused, parentFocus) => <NavBar {...{ focused, parentFocus }} />}
</Focusable>
<Focusable id="main" ref={mainRef} nextFocusUp={navRef}>
{(focused, parentFocus) => (
<FocusableButtons focused={focused} parentFocus={parentFocus} />
)}
</Focusable>
</View>
);
}
function FocusableButtons({ focused, parentFocus }) {
const buttonRefs = React.useRef([]);
useInitialFocus(focused, buttonRefs[0]);
return buttons.map((button, index) => (
<Focusable
id={button.id}
ref={buttonRefs[index]}
nextFocusUp={ buttonRefs?.[index - 1] || parentFocus.nextFocusUp}
nextFocusDown={ buttonRefs?.[index + 1] }
>
{ (focused) => <Button state={focused} >}
</Focusable>
))
}

This hook also allows to memorize which nested focusable was selected, and restore that when going back to that component. The hook will return the update function that you will need to call with the current index. The best way to do it is to call the updater function on every onFocus.

function FocusableButtons({ focused, parentFocus }) {
const buttonRefs = React.useRef([]);
const initialFocusOptions = {
withStateMemory: true
refsList: buttonRefs,
}
const updateFocusedIndex = useInitialFocus(focused, buttonRefs[0], initialFocusOptions);
const onFocus = (element, renderArgs) => {
const { index } = renderArgs;
updateFocusedIndex(index);
};
return buttons.map((button, index) => (
<Focusable
id={button.id}
ref={buttonRefs[index]}
onFocus={onFocus}
nextFocusUp={ buttonRefs?.[index - 1] || parentFocus.nextFocusUp}
nextFocusDown={ buttonRefs?.[index + 1] }
>
{ (focused) => <Button state={focused} >}
</Focusable>
))
}

Last but not Least, since creating these lists of Focusable is a fairly common usecase, we've wrapped up most of the required logic around FlatList with our focusable, and expose a ready to use component called FocusableList.

function FocusableButtons({ focused, parentFocus }) {
const flatListRef = React.useRef(null);
function renderItem({ item, index, focused, parentFocus }) {
return <Button state={focused} button={item} key={index} />;
}
function onListElementPress(element, { item, index }) {
// do something with the focused node or the item in the data
}
function onListElementFocus(element, { item, index }) {
// do something when an item is focused, for instance scroll
// to that index
flatListRef?.current.scrollToIndex?.({
animated: true,
index,
});
}
return (
<FocusableList
id="list-id"
focused={focused}
ref={flatListRef}
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id}
onListElementPress={onListElementPress}
onListElementFocus={onListElementFocus}
{...{ ...parentFocus }}
/>
);
}

API - how to use

@applicaster/zapp-react-native-utils/focusManager

  • useInitialFocus: React hook which will set the provided initialRef as focused when the component is acquiring focus itself. Params:
    • focused: Boolean flag which indicates whether or not the current component is focused
    • initialRef: React.Ref Reference of the children focusable which needs to acquire focus when the component acquires focus itself
    • options:
      • withStateMemory: Boolean flag to allow this hook to remember the previously selected children focusable, and restore it next time the component acquires focus
      • refsList: array of refs used. This is needed if the above option is set to true, in order to restore focus when navigating back to the component
  • useFocusEffect: React hook called on each focused state change. The syntax is the same as for useEffect: React component used to allow focus to be acquired
import { useFocusEffect } from "@applicaster/zapp-react-native-utils/focusManager";
function MyComponent(props) {
const focusRef = React.useRef(null)
useFocusEffect(() => {
// Invoke on focused changing to true
return () => {
// Invoke on focused changes to false
}
}, [props.focused])
return (
<Focusable ref={focusRef} {...focusableProps}>
{ (focused, parentFocus) => ...}
</Focusable>
)
}

@applicaster/zapp-react-native-ui-components/Components/Focusable

Usage:

import { Focusable } from "@applicaster/zapp-react-native-ui-components/Components/Focusable";
function MyComponent(props) {
const focusRef = React.useRef(null)
return (
<Focusable ref={focusRef} {...focusableProps}>
{ (focused, parentFocus) => ...}
</Focusable>
)
}
ParentFocus = {
nextFocusDown: React.Ref,
nextFocusUp: React.Ref,
nextFocusLeft: React.Ref,
nextFocusRight: React.Ref,
}

Here are the props for the Focusable component Items with the * are required

  • *ref: React.Ref reference for the focusable

  • *id: String each focusable needs to have a unique string identifier.

  • children: (focused: boolean, parentFocus: ParentFocus) => React.Node As explained in the examples above, the children of the Focusable component needs to be a function, and it will inject the focused state as well as the next focus values for the parent

  • nextFocusUp?: React.Ref

  • nextFocusDown?: React.Ref

  • nextFocusLeft?: React.Ref

  • nextFocusRight?: React.Ref References of the react node where the next navigation should go in each direction. if left undefined, focus won't move. assign it to parentFocus.nextFocus<Up|Down|Left|Right> to delegate the selection of the next focusable in a given direction to what is set on the parent

  • onFocus: (React.Ref) => void Function called when the center key of the D-pad is pressed. will be invoked with ref of the currently focused focusable

  • onBlur: (React.Ref) => void Function called when a Focusable is losing focused. will be invoked with the ref of the focusable losing focus

  • onPress: (React.Ref) => void Function called when a Focusable is acquiring focus. will be invoked with the ref of the focusable acquiring focus

@applicaster/zapp-react-native-ui-components/Components/FocusableList React component to render a list of focusable, using React Native's FlatList component.

While Focusable can be used on all platforms, FocusableList can only be used on Android TV for now

usage

import { FocusableList } from "@applicaster/zapp-react-native-ui-components/Components/FocusableList";
function MyComponent(props) {
return <FocusableList {...focusableListProps} />;
}

This component is basically wrapper around React Native's FlatList component, so it accepts all the props available to that component. You can see the list of available props here https://reactnative.dev/docs/flatlist#props

On top of this, this component is accepts the following props

Items with the * are required

  • *focused: boolean - Focused flag from the parent Focusable that needs to be forwarded.

  • *renderItem: ({ item, index: number, focused: boolean, parentFocus: ParentFocus, parentRef: React.Ref }) => React.Node

    This prop exists in the FlatList component, but we are overloading it with additional arguments which can be useful in this context. On Top of the data to render, and the index, you also have access to the focused state, the parentFocus object described above, and the ref of the parent focusable

  • *id: string id for the list

  • ref: React.Ref React Ref passed to the FocusableList. This will be forwarded to the underlying Flatlist and can be used to invoke the FlatList's methods

  • horizontal: boolean Optional prop to toggle on if the FlatList needs to render horizontally

  • numColumns: number Optional prop to display the list horizontally as a grid

  • onListElementFocus: (React.Ref, { item, index }, { direction }) => void This function will be called when an element of the list acquires focus. This is invoked with the ref of the focused element, the underlying data from the FlatList's renderItem function, and direction of the focus movement up/down/right/left

  • onListElementBlur: (React.Ref, { item, index }, { direction }) => void This function will be called when an element of the list loses focus. This is invoked with the ref of the focused element, the underlying data from the FlatList's renderItem function and direction of the focus movement up/down/right/lef

  • onListElementPress: (React.Ref, { item, index }) => void This function will be called when an element of the list has the focus and the center key of the D-pad is pressed. This is invoked with the ref of the focused element, and the underlying data from the FlatList's renderItem function

  • *keyExtractor: (item, index) => string Function used to retrieve a unique id to pass to each focusable element. It is invoked with the item and index from the FlatList renderItem function

  • extraChildrenData: any - Value will be passed to the render-Item Pure Component and compared using R.equals (Ramda). Change of the values between renders will cause list children to re-render.

  • nextFocusDown: React.Ref<any>

  • nextFocusUp: React.Ref<any>

  • nextFocusRight: React.Ref<any>

  • nextFocusLeft: React.Ref<any> References of the react node where the next navigation should go in each direction. if left undefined, focus won't move. assign it to parentFocus.nextFocus<Up|Down|Left|Right> to delegate the selection of the next focusable in a given direction to what is set on the parent

  • focusableItemProps: {} Optional prop forwarded to the underlying Focusable elements created inside the FlatList