Skip to content

Commit

Permalink
chore(example): add swiping with AppleTV controller (#67)
Browse files Browse the repository at this point in the history
* chore: add emitKeyDown method to RemoteControlManager

* chore: allow to swipe with ApppleTV controller

* refactor: improve PanEvent using class instead of object
  • Loading branch information
remilry authored Feb 16, 2024
1 parent 5cbede0 commit b85007d
Show file tree
Hide file tree
Showing 13 changed files with 168 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { ProgramDetail } from './src/pages/ProgramDetail';
import { NonVirtualizedGridPage } from './src/pages/NonVirtualizedGridPage';
import { GridWithLongNodesPage } from './src/pages/GridWithLongNodesPage';
import { useTVPanEvent } from './src/components/PanEvent/useTVPanEvent.ios';

const Stack = createNativeStackNavigator<RootStackParamList>();

Expand Down Expand Up @@ -58,6 +59,7 @@ const TabNavigator = () => {
};

function App(): JSX.Element {
useTVPanEvent();
const { height, width } = useWindowDimensions();
const areFontsLoaded = useFonts();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const GRID_SIZE = 1920;
export const NUMBER_OF_COLUMNS = 5;
export const EMIT_KEY_DOWN_INTERVAL = 30;
export const THROTTLE_DELAY_MS = 30;
31 changes: 31 additions & 0 deletions packages/example/src/components/PanEvent/PanEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { getGridCoordinates, moveFocus } from './PanEvent.utils';

class PanEvent {
private orientation = undefined;
private lastIndex = 0;

reset = () => {
this.orientation = undefined;
this.lastIndex = 0;
};
handlePanEvent = ({ x, y }: { x: number; y: number }) => {
const newIndex = getGridCoordinates(x, y, this);
if (!newIndex) return;
moveFocus(newIndex, this);
};

getOrientation = () => {
return this.orientation;
};
setOrientation = (orientation: string) => {
this.orientation = orientation;
};
getLastIndex = () => {
return this.lastIndex;
};
setLastIndex = (lastIndex: number) => {
this.lastIndex = lastIndex;
};
}

export default PanEvent;
54 changes: 54 additions & 0 deletions packages/example/src/components/PanEvent/PanEvent.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { repeat } from '../../utils/repeat';
import RemoteControlManager from '../remote-control/RemoteControlManager';
import { SupportedKeys } from '../remote-control/SupportedKeys';
import PanEvent from './PanEvent';
import { EMIT_KEY_DOWN_INTERVAL, GRID_SIZE, NUMBER_OF_COLUMNS } from './PanEvent.constants';

export const getGridCoordinates = (x: number, y: number, panEvent: PanEvent): number => {
const gridElementSize = GRID_SIZE / NUMBER_OF_COLUMNS;

const xIndex = Math.floor((x + gridElementSize / 2) / gridElementSize);
const yIndex = Math.floor((y + gridElementSize / 2) / gridElementSize);

if (!panEvent.getOrientation()) {
// Lock orientation after significant movement to avoid sliding in two directions
if (xIndex !== panEvent.getLastIndex()) {
panEvent.setOrientation('x');
return xIndex;
}
if (yIndex !== panEvent.getLastIndex()) {
panEvent.setOrientation('y');
return yIndex;
}
return;
}

if (panEvent.getOrientation() === 'x' && xIndex !== panEvent.getLastIndex()) {
return xIndex;
}

if (panEvent.getOrientation() === 'y' && yIndex !== panEvent.getLastIndex()) {
return yIndex;
}
};

export const moveFocus = (index: number, panEvent: PanEvent) => {
const indexDif = index - panEvent.getLastIndex();
panEvent.setLastIndex(index);

if (panEvent.getOrientation() === 'x') {
repeat(
() =>
RemoteControlManager.emitKeyDown(indexDif > 0 ? SupportedKeys.Right : SupportedKeys.Left),
EMIT_KEY_DOWN_INTERVAL,
Math.abs(indexDif),
);
}
if (panEvent.getOrientation() === 'y') {
repeat(
() => RemoteControlManager.emitKeyDown(indexDif > 0 ? SupportedKeys.Down : SupportedKeys.Up),
EMIT_KEY_DOWN_INTERVAL,
Math.abs(indexDif),
);
}
};
21 changes: 21 additions & 0 deletions packages/example/src/components/PanEvent/panEventHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { HWEvent } from 'react-native';
import { throttle } from '../../utils/throttle';

import PanEvent from './PanEvent';
import { THROTTLE_DELAY_MS } from './PanEvent.constants';

const myPanEvent = new PanEvent();

export const panEventHandler = (event: HWEvent) => {
throttle(() => {
if (event.eventType === 'pan') {
if (!event.body) return;
if (event.body.state === 'Began') {
myPanEvent.reset();
}
if (event.body.state === 'Changed') {
myPanEvent.handlePanEvent({ x: event.body.x, y: event.body.y });
}
}
}, THROTTLE_DELAY_MS)();
};
13 changes: 13 additions & 0 deletions packages/example/src/components/PanEvent/useTVPanEvent.ios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useEffect } from 'react';
import { TVEventControl, useTVEventHandler } from 'react-native';
import { panEventHandler } from './panEventHandler';

export const useTVPanEvent = () => {
useEffect(() => {
TVEventControl.enableTVPanGesture();
return () => {
TVEventControl.disableTVPanGesture();
};
}, []);
useTVEventHandler(panEventHandler);
};
3 changes: 3 additions & 0 deletions packages/example/src/components/PanEvent/useTVPanEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const useTVPanEvent = () => {
return null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ class RemoteControlManager implements RemoteControlManagerInterface {
removeKeydownListener = (listener: (event: SupportedKeys) => void) => {
this.eventEmitter.off('keyDown', listener);
};

emitKeyDown = (key: SupportedKeys) => {
this.eventEmitter.emit('keyDown', key);
};
}

export default new RemoteControlManager();
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { SupportedKeys } from './SupportedKeys';
export interface RemoteControlManagerInterface {
addKeydownListener: (listener: (event: SupportedKeys) => void) => void;
removeKeydownListener: (listener: (event: SupportedKeys) => void) => void;
emitKeyDown: (key: SupportedKeys) => void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ class RemoteControlManager implements RemoteControlManagerInterface {
removeKeydownListener = (listener: (event: SupportedKeys) => void) => {
this.eventEmitter.off('keyDown', listener);
};

emitKeyDown = (key: SupportedKeys) => {
this.eventEmitter.emit('keyDown', key);
};
}

export default new RemoteControlManager();
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ class RemoteControlManager implements RemoteControlManagerInterface {
removeKeydownListener = (listener: (event: SupportedKeys) => void) => {
this.eventEmitter.off('keyDown', listener);
};

emitKeyDown = (key: SupportedKeys) => {
this.eventEmitter.emit('keyDown', key);
};
}

export default new RemoteControlManager();
12 changes: 12 additions & 0 deletions packages/example/src/utils/repeat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const repeat = (callback: () => void, delay: number, repetitions: number) => {
let repeatsLeft = repetitions;

const interval = setInterval(() => {
if (repeatsLeft === 0) {
clearInterval(interval);
return;
}
callback();
repeatsLeft--;
}, delay);
};
15 changes: 15 additions & 0 deletions packages/example/src/utils/throttle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const throttle = (callback, delay) => {
let wait = false;

return (...args) => {
if (wait) {
return;
}

callback(...args);
wait = true;
setTimeout(() => {
wait = false;
}, delay);
};
};

0 comments on commit b85007d

Please sign in to comment.