diff --git a/packages/example/App.tsx b/packages/example/App.tsx index 00b203df..a14068fa 100644 --- a/packages/example/App.tsx +++ b/packages/example/App.tsx @@ -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(); @@ -58,6 +59,7 @@ const TabNavigator = () => { }; function App(): JSX.Element { + useTVPanEvent(); const { height, width } = useWindowDimensions(); const areFontsLoaded = useFonts(); diff --git a/packages/example/src/components/PanEvent/PanEvent.interface.ts b/packages/example/src/components/PanEvent/PanEvent.interface.ts new file mode 100644 index 00000000..ee50d659 --- /dev/null +++ b/packages/example/src/components/PanEvent/PanEvent.interface.ts @@ -0,0 +1,6 @@ +export interface PanEventInterface { + orientation: 'x' | 'y'; + lastIndex: number; + reset: () => void; + handlePanEvent: (event: { x: number; y: number }) => void; +} diff --git a/packages/example/src/components/PanEvent/panEventHandler.ts b/packages/example/src/components/PanEvent/panEventHandler.ts new file mode 100644 index 00000000..afc46bc8 --- /dev/null +++ b/packages/example/src/components/PanEvent/panEventHandler.ts @@ -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), + ); + } +}; diff --git a/packages/example/src/components/PanEvent/useTVPanEvent.ios.ts b/packages/example/src/components/PanEvent/useTVPanEvent.ios.ts new file mode 100644 index 00000000..7af77944 --- /dev/null +++ b/packages/example/src/components/PanEvent/useTVPanEvent.ios.ts @@ -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); +}; diff --git a/packages/example/src/components/PanEvent/useTVPanEvent.ts b/packages/example/src/components/PanEvent/useTVPanEvent.ts new file mode 100644 index 00000000..4f6e35ec --- /dev/null +++ b/packages/example/src/components/PanEvent/useTVPanEvent.ts @@ -0,0 +1,3 @@ +export const useTVPanEvent = () => { + return null; +}; diff --git a/packages/example/src/utils/repeat.ts b/packages/example/src/utils/repeat.ts new file mode 100644 index 00000000..c8862c3f --- /dev/null +++ b/packages/example/src/utils/repeat.ts @@ -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); +}; diff --git a/packages/example/src/utils/throttle.ts b/packages/example/src/utils/throttle.ts new file mode 100644 index 00000000..729dde7f --- /dev/null +++ b/packages/example/src/utils/throttle.ts @@ -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); + }; +};