Skip to main content

Flexn Focus Manager

Focus Manager in React Native

Focus Manager is an algorithm for TV's which controls focus. Based on your application structure and remote controller actions decides what to focus next. React Native doesn't have any specific Focus Manager implementation it utilizing native focus manager and exports some API functions for React Native developers to help control some parts of it.

Why it doesn't work well

First we need to understand that whole communication between Javascript and native code happening via React Native Bridge which is capable to connect these two worlds. Native Focus Manager works in a way that whole decision making about what to focus next, what's initial focus and so on happens on native side and we as javascript developers have to obey to those decisions. The problem is that those decisions are not always right because of various reasons like:

  • Each application has it own layout which sometimes can be very complex;
  • Sometimes our application have items which is not perfectly aligned;
  • We have different navigation paradigms in our apps;
  • and so on...

Therefore if we have real world application(something way more complex than "Hello world") we're start hitting the problem when native focus manager proving us wrong decisions and focus start working not as we expecting...

To have better control over focus manager Google added API which allows developers based on their applications force Focus Manager to do what it wants. In React Native that works in a way that focusable item needs to provide reference to another focusable item. An example:

import * as React from 'react';
import { findNodeHandle, View, TouchableOpacity } from 'react-native';

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

React.useEffect(() => {
ref1.current.setNativeProps({
setNextFocusUp: findNodeHandle(ref2.current),
});
ref2.current.setNativeProps({
setNextFocusUp: findNodeHandle(ref3.current),
});
ref3.current.setNativeProps({
setNextFocusDown: findNodeHandle(ref2.current),
});
}, []);

return (
<View>
<TouchableOpacity ref={ref1} />
<TouchableOpacity ref={ref2} />
<TouchableOpacity ref={ref3} />
</View>
);
};

export default MyComponent;

So in this example for Native Focus Manager we're saying that once first TouchableOpacity has focus gained his next up direction will be second TouchableOpacity, second TouchableOpacity on up command will focus last one and last on once down button is pressed will move focus back to 2nd TouchableOpacity. Well it looks like we have some control, but it has some fundamental issues like:

  • We are communicating with native code and React Native bridge has delay, so usually these focus enforcements makes focus manager do 2 actions: first native default one and second our which we wrote in javascript which eventually it can make very bad user experience;
  • Having complex apps which has a lot of different components and screens managing these references become tremendous work which also makes our code base very fragile;
  • This focus enforcement was working on Android TV only, before React Native cut down support for TV's at all and this library was created which added support for tvOS also, but it works in slightly different way.

Flexn Focus Manager

With Flexn Focus Manager we took different approach. After lot of discussions and investigation what we can do that javascript and native code communication regards to focus management could be reliable enough and solve fundamental problems described above we came to the solution that we should remove native part from equation and turn all decision making to Javascript where we have best control over our applications.

We still utilizing the best of native and created components library which puts all heavy operations and requires best code performance like focus animations to native code. Having this combination in mind we managed to create Flexn Focus Manager which finally works!

How to use it

Nevertheless Flexn Focus Manager is simple enough it has some fundamental rules which we need to comply to use it right. All primitive components must be exported from @flexn/create since each of them are designed for Flexn Focus Manager and helps it to understand our application layout. API for each component is described in section above so here we will go through some real app examples and rules.

Root of your app

Root of your application must be wrapped with import { App } from '@flexn/create'; it's a single import which initialize Flexn Focus Manager events to be accessible within whole app.

import * as React from 'react';
import { App } from '@flexn/create';

const MyApp = () => {
return <App>...rest of your code</App>;
};

export default MyApp;

Screens

Screen represents collection of the children's which belongs only for particular block. Even though screen usually is wrapping separate pages of your application, but it not necessary has to be like that. With Screen you can create things like modals which overlaps the context, side navigation which typically always visible for user whatever you navigate or anything else depends on your application structure. There are few important rules working with screens:

  • You can never wrap screen into another screen. That doesn't work;
  • Screen must have states. More about those below;

State of the screen tells focus manager whenever your screen is in foreground and visible for user or it's moved to background. Example bellow shows simple implementation with react-navigation.

import * as React from 'react';
import { App, Screen, Text, Pressable, StyleSheet, ScreenStates } from '@flexn/create';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { NavigationContainer, useFocusEffect } from '@react-navigation/native';

const Stack = createNativeStackNavigator();

const Screen1 = ({ navigation }) => {
const [screenState, setScreenState] = React.useState < ScreenStates > 'foreground';

useFocusEffect(
React.useCallback(() => {
setScreenState('foreground');
return () => {
setScreenState('background');
};
}, [])
);

return (
<Screen style={styles.container} screenState={screenState}>
<Pressable style={styles.button} onPress={() => navigation.navigate('myScreen2')}>
<Text>Navigate to screen 2</Text>
</Pressable>
<Pressable style={styles.button}>
<Text>Empty action button</Text>
</Pressable>
</Screen>
);
};

const Screen2 = ({ navigation }) => {
const [screenState, setScreenState] = React.useState < ScreenStates > 'foreground';

useFocusEffect(
React.useCallback(() => {
setScreenState('foreground');
return () => {
setScreenState('background');
};
}, [])
);

return (
<Screen style={styles.container} screenState={screenState}>
<Pressable style={styles.button} onPress={() => navigation.navigate('myScreen1')}>
<Text>Back to screen 1</Text>
</Pressable>
<Pressable style={styles.button}>
<Text>Empty action button</Text>
</Pressable>
</Screen>
);
};

const MyApp = () => {
return (
<App>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="myScreen1" component={Screen1} />
<Stack.Screen name="myScreen2" component={Screen2} />
</Stack.Navigator>
</NavigationContainer>
</App>
);
};

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#FFFFFF',
},
button: {
margin: 20,
borderWidth: 2,
borderRadius: 25,
borderColor: '#111111',
height: 50,
width: 200,
justifyContent: 'center',
alignItems: 'center',
},
});

export default MyApp;

There are only two states you have to deal with

  • foreground means screen is active in the focus finder;
  • background screen is removed from the focus finder;

As example above shows we're changing screen state based on your activity. If we are in Screen1 we setting screen state as foreground and once we leave that screen background state is applied. Same story with Screen2.

Full screen API can be found here.

Working with modals

The tricky thing with modal is that usually Modal is opened on the top of already active content. In this case focus finder somehow should recognize that we want to search for next focusable only in overflowing modal and what if there are more that one Modal on the screen?

For situation described above Screen offers property called screenOrder which allows different screens to overlap which each other at the same time keeping focus to the right place.

import * as React from 'react';
import { App, Screen, Text, Pressable, StyleSheet } from '@flexn/create';

const MyApp = () => {
const [modalOpen, setModalOpen] = React.useState(false);

const renderModal = () => {
if (modalOpen) {
return (
<Screen style={styles.modal} screenOrder={1}>
<Pressable style={styles.button}>
<Text>Hello from Flexn Create</Text>
</Pressable>
<Pressable style={styles.button}>
<Text>Hello from Flexn Create</Text>
</Pressable>
<Pressable style={styles.button} onPress={() => setModalOpen(false)}>
<Text>Close modal</Text>
</Pressable>
</Screen>
);
}

return null;
};

return (
<App style={{ flex: 1, width: '100%', height: '100%' }}>
<Screen style={styles.container}>
<Pressable style={styles.button} onPress={() => setModalOpen(true)}>
<Text>Open modal</Text>
</Pressable>
</Screen>
{renderModal()}
</App>
);
};

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#FFFFFF',
},
modal: {
position: 'absolute',
left: 150,
top: 75,
right: 150,
bottom: 75,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'red',
},
button: {
margin: 20,
borderWidth: 2,
borderRadius: 25,
borderColor: '#111111',
height: 50,
width: 200,
justifyContent: 'center',
alignItems: 'center',
},
});

export default MyApp;

By default screenOrder has 0 value. So in this case setting anything else bigger than 0 allows us keep multiple foreground screens in the viewport at the same time overflowing each other.

Creating custom components

Flexn Focus Manager understand app layout by creating context for each element in focus equation. Context holds all necessary information of element for focus manager. All primitive components which is exported from @flexn/create taking care of holding and passing context down to other elements by itself, but if you have created your custom component this needs to be done by manually(which is very easy):

import * as React from 'react';
import { App, Screen, Text, Pressable, StyleSheet } from '@flexn/create';

const MyCustomComponent = ({ parentContext }: { parentContext?: any }) => {
return (
<Pressable parentContext={parentContext} style={styles.focusableElement}>
<Text>Hello from Flexn Create</Text>
</Pressable>
);
};

const MyApp = () => {
return (
<App>
<Screen style={styles.container}>
<Pressable style={styles.focusableElement}>
<Text>Hello from Flexn Create</Text>
</Pressable>
<Pressable style={styles.focusableElement}>
<Text>Hello from Flexn Create</Text>
</Pressable>
<MyCustomComponent />
</Screen>
</App>
);
};

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
focusableElement: {
width: 250,
height: 250,
backgroundColor: '#0A74E6',
margin: 20,
},
});

export default MyApp;

As you can see in this example we are creating our custom component called MyCustomComponent which by default inherits special property called parentContext which needs to be passed down to your parent component of return function. After doing that Flexn Focus Manager will take care of the rest.

IMPORTANT: do not create property for any of your custom component called parentContext it's reserved by Flexn Focus Manager.