Skip to content

Commit

Permalink
chore: allow to swipe with ApppleTV controller
Browse files Browse the repository at this point in the history
  • Loading branch information
remilry committed Feb 15, 2024
1 parent 78d9cc8 commit 85d840f
Show file tree
Hide file tree
Showing 7 changed files with 137 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,6 @@
export interface PanEventInterface {
orientation: 'x' | 'y';
lastIndex: number;
reset: () => void;
handlePanEvent: (event: { x: number; y: number }) => void;
}
86 changes: 86 additions & 0 deletions packages/example/src/components/PanEvent/panEventHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { HWEvent } from 'react-native';
import RemoteControlManager from '../remote-control/RemoteControlManager';
import { SupportedKeys } from '../remote-control/SupportedKeys';
import { PanEventInterface } from './PanEvent.interface';
import { throttle } from '../../utils/throttle';
import { repeat } from '../../utils/repeat';

const GRID_SIZE = 1920;
const NUMBER_OF_COLUMNS = 5;

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

export const panEventHandler = (event: HWEvent) => {
throttle(() => {
if (event.eventType === 'pan') {
if (!event.body) return;
if (event.body.state === 'Began') {
PanEvent.reset();
}
if (event.body.state === 'Changed') {
PanEvent.handlePanEvent({ x: event.body.x, y: event.body.y });
}
}
}, 30)();
};

const getGridCoordinates = (x: number, y: number): 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.orientation) {
// Lock orientation after significant movement to avoid sliding in two directions
if (xIndex !== PanEvent.lastIndex) {
PanEvent.orientation = 'x';
return xIndex;
}
if (yIndex !== PanEvent.lastIndex) {
PanEvent.orientation = 'y';
return yIndex;
}
return;
}

if (PanEvent.orientation === 'x' && xIndex !== PanEvent.lastIndex) {
return xIndex;
}

if (PanEvent.orientation === 'y' && yIndex !== PanEvent.lastIndex) {
return yIndex;
}
};

const moveFocus = (index: number) => {
const indexDif = index - PanEvent.lastIndex;
PanEvent.lastIndex = index;

if (PanEvent.orientation === 'x') {
repeat(
() =>
RemoteControlManager.emitKeyDown(indexDif > 0 ? SupportedKeys.Right : SupportedKeys.Left),
30,
Math.abs(indexDif),
);
}
if (PanEvent.orientation === 'y') {
repeat(
() => RemoteControlManager.emitKeyDown(indexDif > 0 ? SupportedKeys.Down : SupportedKeys.Up),
30,
Math.abs(indexDif),
);
}
};
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;
};
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--;
}, 30);
};
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 85d840f

Please sign in to comment.