Focus Management with React Native on TV Platforms

This is a guest contribution from Guillermo Velasquez, a React Native Consultant at G2i.

Over the past couple of months, we’ve been building a React Native-based streaming video app for mobile, tablet, and TV devices using You.i TV’s cross-platform SDK, You.i Engine One. We’ve talked before about our experience working with You.i Engine One as a whole but today we want to dig down deep and talk about a feature that we really enjoy: the FocusManager.

When developing applications for connected TV and Smart TV platforms, it’s important to remember that user input comes from a remote.

Shocking, right?

Now that I’ve stated the obvious, how do we, as developers, handle and give proper feedback to the user when they are interacting with said remote? Enter Focus Management.

 

Photo by rawpixel

 

Focus management is required to make TV navigation easy for users. Without it, there is no way to let the user know if a visual element — like selecting an image or button — requires any further action. So, to build an app for a TV platform, we need a way to handle and give feedback about the focus of UI elements within the app. That’s where You.i Engine One’s FocusManager comes in.

 

Focus Management With You.i Engine One

When we first started developing for TV using You.i Engine One and its React Native solution, we were emulating the TV UI on a macOS app. So, we started developing features and using our pointer to interact with the UI. It wasn’t until later, when tickets about focus management started to show up, that we started testing on Android TV and used the remote control.

Now what?

You.i Engine One is built for TV

When we started paying attention to focus management, we went to You.i TV’s documentation. That’s when it became clear that You.i Engine One was developed with TV in mind. We found a full explanation on why focus management is important.

Moreover, we could rely on the same techniques and APIs for any TV platform supported by You.i Engine One. By the time of writing this piece, tvOS, Android TV, Amazon Fire TV, Roku, PlayStation 4, and Xbox One are all included.

How does it work?

The fundamentals behind focus management are the same between platforms: simple navigation and highlighting the selected element. Differences arise when implementing how the focus behaves.

By default, FocusManager only needs FocusIn and FocusOut timelines to make something focusable. This is done when using You.i Engine One’s After Effects Compositions.

 

 

After the timeline hooks up to an item, the engine will pick it up by default. Then you’ll be able to see it getting focused by using the remote control on your platform or emulator. In practice, all it will do is play a certain animation that gives the user feedback on what it is happening on the screen.

So you should always have FocusIn and FocusOut timelines for any Ref to get it focused. If there is no focus animation on them, the user will never know if the particular view is selected or not.

 

Advanced Techniques with FocusManager

In most cases, the default focus behavior is enough for apps built with You.i Engine One. If we need to tweak the default behavior, we can do so. There are two main ways we can interact with the focusability of elements:

Hooking up onFocus and onBlur props to focusable elements

Once we have made an element focusable, we can hook up to its focus and blur events. This is particularly helpful when we want to hook up extra logic on top of the animations to the UI elements. For example:

 

import * as React from 'react';
import { ButtonRef } from '@youi/react-native-youi';

class PressMeButton extends React.Component {
  render() {
    return (
      <ButtonRef
        name="Btn-PressMe"
        onFocus={ () => console.log('I gained focus'); }
        onBlur={ () => console.log('I lost focus'); }
        onPress={ () => console.log('I was pressed'); }
      >
        <TextRef name="Text" text="Press Me!" />
      </ButtonRef>
    );
  }
}

Using the FocusManager module

You.i Engine One exposes the FocusManager module which lets us override the default behavior.

The FocusManager module exposes the following API:

 

interface FocusManagerInterface {
    getTag(refOrTag: Ref): void;

    /**
     * Requests that focus be moved to the component with the given tag.
     *
     * @param refOrTag The ref or a node handle of the component.
     */
    focus(refOrTag: Ref): void;

    /**
     * Controls whether the component with the given ref/tag is a focus root.
     *
     * @param refOrTag The ref or a node handle of the component.
     * @param isFocusRoot True if the component should be a focus root; 
     *                    false otherwise.
     */
    setFocusRoot(refOrTag: Ref, isFocusRoot: boolean): void;

    /**
     * Enables the focusability of the component with the given tag.
     * 
     * @param refOrTag The ref or a node handle of the component.
     */
    enableFocus(refOrTag: Ref): void;

    /**
     * Disables the focusability of the component with the given tag.
     * 
     * @param refOrTag The ref or a node handle of the component.
     */
    disableFocus(refOrTag: Ref): void;

    /**
     * Sets the focusability of the component 
     * with the given tag to the provided value.
     * 
     * @param refOrTag The ref or a node handle of the component.
     * @param focusable The boolean value representing whether 
     *                  the component should be focusable or not.
     */
    setFocusable(refOrTag: Ref, focusable: boolean): void;

    /**
     * Sets the focus path between the two components with 
     * the given refs/tags in the given direction.
     *
     * @param fromRefOrTag The ref or a node handle of the component 
     *                     from which the focus path originates.
     * @param toRefOrTag The ref or a node handle of the component 
     *                   at which the focus path terminates.
     * @param focusDirection The direction of the focus path. 
     *                       Valid directions are: 
     *                       "up", "down", "right", "left", "forward" and "reverse".
     */
    setNextFocus(
      fromRefOrTag: Ref,
      toRefOrTag: Ref,
      focusDirection: 'up' | 'down' | 'right' | 'left' | 'forward' | 'reverse',
    ): void;
  }

With the Focus Manager, we can do pretty cool stuff, like:

1. Autofocus: A great user experience eases navigation and guides the user. For example, when filling out a form in an app, the focus inputs should guide where we want the user to enter information. The same notion applies to TV platforms. When a user goes to a particular screen, we want to focus on the next immediate action:

 

import * as React from 'react';
import { ButtonRef, FocusManager } from '@youi/react-native-youi';

class PressMeButton extends React.Component {
  // When the button is loaded, we tell the FocusManager to set focus on it
  render() {
    return (
      <ButtonRef
        name="Btn-PressMe"
        onLoad={ (ref: ButtonRef) => { ref && FocusManager.focus(ref); }
        onFocus={ () => console.log('I gained focus'); }
      >
        <TextRef name="Text" text="Press Me!" />
      </ButtonRef>
    );
  }
}

2. Overriding focusable elements: The default focusability of elements in You.i Engine One is somewhat related to the position of elements on screen. Take a look at the following image.

 

A linear list of elements, focusable all in line to the right.

 

The expected focusability of this list when pressing right on the controller would be: A, B, C and D. As we should expect. However, look at the next two images.

 

 

The ideal focusability of elements.

 

Focus Manager assumes that C is the next element to A.

 

The image on the left shows how we want the focusability top work when pressing “right” on our remote. Yet, as the FocusManager maps elements upon their position, it assumes that the next element to A is C.

To override this behavior, we must use the setNextFocus method call like this:

 

import * as React from 'react';
import { ViewRef, ButtonRef, FocusManager } from '@youi/react-native-youi';

class Alphabet extends React.Component {
  // Let's map all references upon loading.
  this.buttonRefs = {};
  onLoadBtn = (button: String, ref: ButtonRef) => this.buttons[button] = ref;
  
  // Now let's override right navigation to match the first image.
  componentDidMount() {
    FocusManager.setNextFocus(this.buttonRefs['a'], this.buttonRefs['b'], "right");
    FocusManager.setNextFocus(this.buttonRefs['b'], this.buttonRefs['c'], "right");
    FocusManager.setNextFocus(this.buttonRefs['c'], this.buttonRefs['d'], "right");
    // We can make it go to the first element once hitting the end.
    FocusManager.setNextFocus(this.buttonRefs['d'], this.buttonRefs['a'], "right");
  }
  
  render() {
    return (
      <ViewRef name="Container">
        <ButtonRef name="Btn-A" onLoad={ ref => this.onLoadBtn('a', ref)} text="A" />
        <ButtonRef name="Btn-B" onLoad={ ref => this.onLoadBtn('b', ref)} text="B" />
        <ButtonRef name="Btn-C" onLoad={ ref => this.onLoadBtn('c', ref)} text="C" />
        <ButtonRef name="Btn-D" onLoad={ ref => this.onLoadBtn('d', ref)} text="D" />
      </ViewRef>
    );
  }
}

3. Setting a “focus root”: If you want to “lock” on a certain view and its children while ignoring everything around it, you can set up a focus root.

 

Locking focus to a certain ViewRef as the “root”

 

In this example, we want to lock the focusability of elements to those inside the green rectangle. We might want to do that to restrict the user from being able to focus elements outside that green area. This is especially useful when triggering modals. To set up a focus root, we simply write some code similar to the following:

 

import { ViewRef, ButtonRef, FocusManager } from '@youi/react-native-youi';

class Modal extends React.Component {
  this.rootViewRef: ViewRef;
  
  // Let's set the focus root to this view and its children.
  componentDidMount() {
    FocusManager.setFocusRoot(this.rootViewRef, true);    
  }
  
  render() {
    return (
      <ViewRef name="Root" onLoad={ (ref: ViewRef) => this.rootViewRef = ref; }>
        <ButtonRef name="Btn-Ok" text="Okay" />
        <ButtonRef name="Btn-Cancel" text="Cancel" />
      </ViewRef>
    );
  }
}

4. onFocus(or onBlur)InDescendants: This is particularly useful for lists of items when you want to apply the same onFocus or onBlur event to all elements of a list.

 

import { ListRef, FocusManager } from '@youi/react-native-youi';

class FocusDescendans extends React.Component {
  this.rootViewRef: ViewRef;
  onChildFocused = () => { console.log('Look mom, I am focused'); }
  
  render() {
    return (
      <ListRef name="MyList" onLoad={this.onLoad}
        data={...}
        renderItem={...}
        onFocusInDescendants={this.onChildFocused}
      />
    );
  }
}

Bonus: There’s a Focus Debugger!

Remember when I said You.i Engine One was built for TV? Well, it has a nifty debugger with all sorts of cool options. The one place I go to every time I’m developing a feature for a TV platform is the focus debugger.

 

 

It essentially lets you understand where the focus is at the current moment, which element is the focus root, and what are the next focusable items in each direction.

I can’t express how many times this tool saved me hours of poking around and console logging.

Room for improvement

Although the FocusManager is a great tool, it does have a few drawbacks, mainly that its API is imperative. It makes playing nicely with React Native components somewhat troublesome with having to store the ref internally and then imperatively setting focus.

Here’s hoping future versions of You.i Engine One might have the capabilities to have declarative props on Refs like:

  • lockFocus for ViewRefs to automatically setFocusRoot on that ViewRef when mounted and…
  • focus for, similarly, telling the FocusManager to focus an element upon rendering it or updating it so we don’t have to store the ref and imperatively set the focus.

 

What About Vanilla React Native?

At this point, you’re probably wondering why we didn’t just stick to vanilla React Native. After all, the official React Native documentation has an entry about Building For TV Devices. There is both support for Android TV and tvOS. It states that they’ve added support for React Native apps to “just work” when targeting TVs.

It’s fantastic news for development teams tasked with going cross-platform but at the time of writing this piece, there are some shortcomings.

TVs are not the main focus of React Native

From the beginning, the core platforms targeted by RN were mobile ones. It is understandable that, while RN apps could work on TVs, there is a lot of ground to cover, and it seems to not be actively on their pipeline.

In fact, there are some key issues worth noting are:

  • Lack of Documentation: The material available for how to build an app for TVs using React Native is scarce. For instance, it barely references how to interact with UI elements. It mentions how touchable components and the TVEventHandler interface work for making items focusable and handling events. Yet, for someone who has never dealt with Focus Management, it is hard to get started. It is pretty much left to a long trial and error session.
  • Few Platforms: Official support on React Native only covers Android TV and tvOS. If we are targeting an application for several platforms (like Roku, Xbox, PlayStation, etc…), then the official support falls short.

After working with You.i Engine One and having conversations with You.i TV’s React Native team, I know that they are working hard to improve their engine. So far, we at G2i have been pretty impressed with what we are able to achieve with it. As with any tool that is pushing the edge of what it is possible, they are making sure that those little issues get covered along the road.

If you’d like to read more about what we’re up to over at G2i, visit our blog.

Did you like that post? Here’s something similar we think you’ll enjoy.