diff --git a/packages/example/android/app/src/main/java/com/bamlab/hoppix/MainActivity.java b/packages/example/android/app/src/main/java/com/bamlab/hoppix/MainActivity.java index 832c23c0..f3fc4d71 100644 --- a/packages/example/android/app/src/main/java/com/bamlab/hoppix/MainActivity.java +++ b/packages/example/android/app/src/main/java/com/bamlab/hoppix/MainActivity.java @@ -26,7 +26,9 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { // KeyEventModule.getInstance().onKeyDownEvent(keyCode, event); // // Using B. - KeyEventModule.getInstance().onKeyDownEvent(keyCode, event); + if (KeyEventModule.getInstance() != null) { + KeyEventModule.getInstance().onKeyDownEvent(keyCode, event); + } // There are 2 ways this can be done: // 1. Override the default keyboard event behavior @@ -43,7 +45,9 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { @Override // <--- Add this method if you want to react to keyUp public boolean onKeyUp(int keyCode, KeyEvent event) { - KeyEventModule.getInstance().onKeyUpEvent(keyCode, event); + if (KeyEventModule.getInstance() != null) { + KeyEventModule.getInstance().onKeyUpEvent(keyCode, event); + } // There are 2 ways this can be done: // 1. Override the default keyboard event behavior diff --git a/packages/example/src/components/VirtualizedSpatialGrid.tsx b/packages/example/src/components/VirtualizedSpatialGrid.tsx index 2281ed06..9bb78fbe 100644 --- a/packages/example/src/components/VirtualizedSpatialGrid.tsx +++ b/packages/example/src/components/VirtualizedSpatialGrid.tsx @@ -4,6 +4,7 @@ import { StyleSheet, View } from 'react-native'; import { SpatialNavigationVirtualizedGrid } from 'react-tv-space-navigation/src'; import { programInfos } from '../modules/program/infra/programInfos'; import { ProgramNode } from '../modules/program/view/ProgramNode'; +import { scaledPixels } from '../design-system/helpers/scaledPixels'; const NUMBER_OF_ROWS_VISIBLE_ON_SCREEN = 2; const NUMBER_OF_RENDERED_ROWS = NUMBER_OF_ROWS_VISIBLE_ON_SCREEN + 3; @@ -41,11 +42,11 @@ export const VirtualizedSpatialGrid = ({ const styles = StyleSheet.create({ container: { - height: 780, + height: scaledPixels(780), backgroundColor: '#222', - padding: 30, - borderRadius: 20, + padding: scaledPixels(30), + borderRadius: scaledPixels(20), overflow: 'hidden', }, - rowStyle: { gap: 30 }, + rowStyle: { gap: scaledPixels(30) }, }); diff --git a/packages/example/src/design-system/components/Button.tsx b/packages/example/src/design-system/components/Button.tsx new file mode 100644 index 00000000..d9e74047 --- /dev/null +++ b/packages/example/src/design-system/components/Button.tsx @@ -0,0 +1,43 @@ +import { forwardRef } from 'react'; +import { Animated, View } from 'react-native'; +import { SpatialNavigationNode } from 'react-tv-space-navigation/src'; +import { Typography } from './Typography'; +import styled from '@emotion/native'; +import { useFocusAnimation } from '../helpers/useFocusAnimation'; +import { scaledPixels } from '../helpers/scaledPixels'; + +type ButtonProps = { + label: string; + onSelect?: () => void; +}; + +const ButtonContent = forwardRef((props, ref) => { + const { isFocused, label } = props; + const anim = useFocusAnimation(isFocused); + return ( + + {label} + + ); +}); + +ButtonContent.displayName = 'ButtonContent'; + +export const Button = ({ label }: ButtonProps) => { + return ( + + {({ isFocused }) => } + + ); +}; + +const Container = styled(Animated.View)<{ isFocused: boolean }>(({ isFocused, theme }) => ({ + alignSelf: 'baseline', + backgroundColor: isFocused ? 'white' : 'black', + padding: theme.spacings.$4, + borderRadius: scaledPixels(12), +})); + +const ColoredTypography = styled(Typography)<{ isFocused: boolean }>(({ isFocused }) => ({ + color: isFocused ? 'black' : 'white', +})); diff --git a/packages/example/src/design-system/helpers/scaledPixels.ts b/packages/example/src/design-system/helpers/scaledPixels.ts new file mode 100644 index 00000000..55f1545d --- /dev/null +++ b/packages/example/src/design-system/helpers/scaledPixels.ts @@ -0,0 +1,13 @@ +import { Dimensions } from 'react-native'; + +export const screen = Dimensions.get('window'); +const scale = (screen.width || 1920) / 1920; + +/** + * Unfortunately, AndroidTV handles pixels in a strange manner + * PixelRatio does not seem to solve the problem properly on web. + * So we just scale the pixels manually. + * + * https://github.com/react-native-tvos/react-native-tvos/issues/57 + */ +export const scaledPixels = (pixels: number) => pixels * scale; diff --git a/packages/example/src/design-system/helpers/useFocusAnimation.ts b/packages/example/src/design-system/helpers/useFocusAnimation.ts new file mode 100644 index 00000000..b1fb3d1a --- /dev/null +++ b/packages/example/src/design-system/helpers/useFocusAnimation.ts @@ -0,0 +1,17 @@ +import { useEffect, useRef } from 'react'; +import { Animated } from 'react-native'; + +export const useFocusAnimation = (isFocused: boolean) => { + const scaleAnimation = useRef(new Animated.Value(0)).current; + + useEffect(() => { + Animated.spring(scaleAnimation, { + toValue: isFocused ? 1.1 : 1, + useNativeDriver: true, + damping: 10, + stiffness: 100, + }).start(); + }, [isFocused, scaleAnimation]); + + return { transform: [{ scale: scaleAnimation }] }; +}; diff --git a/packages/example/src/design-system/theme/sizes.ts b/packages/example/src/design-system/theme/sizes.ts index ba42c4b1..8d5eb254 100644 --- a/packages/example/src/design-system/theme/sizes.ts +++ b/packages/example/src/design-system/theme/sizes.ts @@ -1,6 +1,8 @@ +import { scaledPixels } from '../helpers/scaledPixels'; + export const sizes = { program: { - landscape: { width: 250, height: 200 }, - portrait: { width: 200, height: 250 }, + landscape: { width: scaledPixels(250), height: scaledPixels(200) }, + portrait: { width: scaledPixels(200), height: scaledPixels(250) }, }, }; diff --git a/packages/example/src/design-system/theme/spacings.ts b/packages/example/src/design-system/theme/spacings.ts index 9c26b48b..a3e8d35c 100644 --- a/packages/example/src/design-system/theme/spacings.ts +++ b/packages/example/src/design-system/theme/spacings.ts @@ -1,17 +1,19 @@ +import { scaledPixels } from '../helpers/scaledPixels'; + export const spacings = { - 0: 0, - '$0.5': 2, - $1: 4, - $2: 8, - $3: 12, - $4: 16, - $5: 20, - $6: 24, - $7: 28, - $8: 32, - $9: 36, - $10: 40, - $12: 48, - $15: 60, - $20: 80, + 0: scaledPixels(0), + '$0.5': scaledPixels(2), + $1: scaledPixels(4), + $2: scaledPixels(8), + $3: scaledPixels(12), + $4: scaledPixels(16), + $5: scaledPixels(20), + $6: scaledPixels(24), + $7: scaledPixels(28), + $8: scaledPixels(32), + $9: scaledPixels(36), + $10: scaledPixels(40), + $12: scaledPixels(48), + $15: scaledPixels(60), + $20: scaledPixels(80), }; diff --git a/packages/example/src/design-system/theme/typography.ts b/packages/example/src/design-system/theme/typography.ts index 63e6d890..75370d9e 100644 --- a/packages/example/src/design-system/theme/typography.ts +++ b/packages/example/src/design-system/theme/typography.ts @@ -1,3 +1,5 @@ +import { scaledPixels } from '../helpers/scaledPixels'; + export const fontFamilies = { montserrat: { medium: 'Montserrat-Medium', @@ -10,25 +12,25 @@ export const typography = { title: { regular: { fontFamily: fontFamilies.montserrat.semiBold, - fontSize: 32, - lineHeight: 40, + fontSize: scaledPixels(32), + lineHeight: scaledPixels(40), }, strong: { fontFamily: fontFamilies.montserrat.bold, - fontSize: 32, - lineHeight: 40, + fontSize: scaledPixels(32), + lineHeight: scaledPixels(40), }, }, body: { regular: { fontFamily: fontFamilies.montserrat.medium, - fontSize: 24, - lineHeight: 32, + fontSize: scaledPixels(24), + lineHeight: scaledPixels(32), }, strong: { fontFamily: fontFamilies.montserrat.semiBold, - fontSize: 24, - lineHeight: 32, + fontSize: scaledPixels(24), + lineHeight: scaledPixels(32), }, }, } as const; diff --git a/packages/example/src/modules/program/view/Program.tsx b/packages/example/src/modules/program/view/Program.tsx index f45eb19d..5ed04223 100644 --- a/packages/example/src/modules/program/view/Program.tsx +++ b/packages/example/src/modules/program/view/Program.tsx @@ -1,7 +1,8 @@ import styled from '@emotion/native'; -import React, { useEffect, useRef } from 'react'; +import React from 'react'; import { Animated, Image, View } from 'react-native'; import { ProgramInfo } from '../domain/programInfo'; +import { useFocusAnimation } from '../../../design-system/helpers/useFocusAnimation'; type ProgramProps = { isFocused?: boolean; @@ -12,20 +13,11 @@ export const Program = React.forwardRef( ({ isFocused = false, programInfo }, ref) => { const imageSource = programInfo.image; - const scaleAnimation = useRef(new Animated.Value(0)).current; - - useEffect(() => { - Animated.spring(scaleAnimation, { - toValue: isFocused ? 1.1 : 1, - useNativeDriver: true, - damping: 10, - stiffness: 100, - }).start(); - }, [isFocused, scaleAnimation]); + const scaleAnimation = useFocusAnimation(isFocused); return ( diff --git a/packages/example/src/modules/program/view/ProgramList.tsx b/packages/example/src/modules/program/view/ProgramList.tsx index 42e7617d..543c88ce 100644 --- a/packages/example/src/modules/program/view/ProgramList.tsx +++ b/packages/example/src/modules/program/view/ProgramList.tsx @@ -8,10 +8,11 @@ import { RootStackParamList } from '../../../../App'; import { ProgramInfo } from '../domain/programInfo'; import { programInfos } from '../infra/programInfos'; import { ProgramNode } from './ProgramNode'; +import { scaledPixels } from '../../../design-system/helpers/scaledPixels'; const NUMBER_OF_ITEMS_VISIBLE_ON_SCREEN = 6; const WINDOW_SIZE = NUMBER_OF_ITEMS_VISIBLE_ON_SCREEN + 8; -const ROW_PADDING = 70; +const ROW_PADDING = scaledPixels(70); export const ProgramList = ({ orientation, @@ -77,6 +78,6 @@ export const ProgramsColumn = ({ containerStyle }: { containerStyle?: object }) const Container = styled.View(({ theme }) => ({ backgroundColor: theme.colors.background.mainHover, padding: theme.spacings.$8, - borderRadius: 20, + borderRadius: scaledPixels(20), overflow: 'hidden', })); diff --git a/packages/example/src/pages/ProgramDetail.tsx b/packages/example/src/pages/ProgramDetail.tsx index ad23f085..b0a8c9e2 100644 --- a/packages/example/src/pages/ProgramDetail.tsx +++ b/packages/example/src/pages/ProgramDetail.tsx @@ -1,12 +1,13 @@ import styled from '@emotion/native'; import { RouteProp } from '@react-navigation/native'; -import { DefaultFocus, SpatialNavigationNode } from 'react-tv-space-navigation/src'; +import { DefaultFocus } from 'react-tv-space-navigation/src'; import { RootStackParamList } from '../../App'; import { Page } from '../components/Page'; import { Box } from '../design-system/components/Box'; import { Spacer } from '../design-system/components/Spacer'; import { Typography } from '../design-system/components/Typography'; import { ProgramListWithTitle } from '../modules/program/view/ProgramListWithTitle'; +import { Button } from '../design-system/components/Button'; export const ProgramDetail = ({ route, @@ -19,24 +20,24 @@ export const ProgramDetail = ({ + + + - - {({ isFocused }) => ( - - - - )} - + + + {programInfo.title} + + + + {programInfo.description} + + +