Skip to content

Commit

Permalink
Add swiper component
Browse files Browse the repository at this point in the history
  • Loading branch information
Madumo committed Oct 2, 2024
1 parent af3002b commit 7ed41cc
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 2 deletions.
1 change: 1 addition & 0 deletions template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@react-native-firebase/remote-config": "^21.0.0",
"@react-navigation/native": "^6.1.9",
"@react-navigation/native-stack": "^6.9.17",
"@shopify/flash-list": "^1.7.1",
"@tanstack/react-query": "^5.12.2",
"axios": "^1.6.2",
"babel-plugin-module-resolver": "^5.0.0",
Expand Down
92 changes: 92 additions & 0 deletions template/src/components/kit/swiper/Swiper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { ContentStyle, FlashList, FlashListProps } from '@shopify/flash-list';
import { View } from 'react-native';
import Animated from 'react-native-reanimated';
import React, { Ref, useImperativeHandle, useMemo, useRef } from 'react';
import { useCenteredTileOffsets } from './hooks';

export interface SwiperProps<TItem>
extends Omit<
FlashListProps<TItem>,
| 'contentContainerStyle'
| 'snapToOffsets'
| 'horizontal'
| 'estimatedItemSize'
> {
containerWidth: number;
containerHorizontalPadding: number;
tileWidth: number;
gap: number;
contentContainerStyle?: Pick<
ContentStyle,
'backgroundColor' | 'paddingVertical' | 'paddingTop' | 'paddingBottom'
>;
footerWidth?: number;
forwardedRef?: Ref<SwiperMethods>;
}

export type SwiperMethods = { scrollToTop: (animated?: boolean) => void };
const AnimatedFlashList = Animated.createAnimatedComponent(FlashList<any>);

export function Swiper<TItem>({
containerWidth,
tileWidth,
gap,
containerHorizontalPadding,
contentContainerStyle,
snapToAlignment = 'start',
scrollIndicatorInsets,
footerWidth = 0,
forwardedRef,
...props
}: SwiperProps<TItem>) {
const internalRef = useRef<any>(null);

useImperativeHandle(
forwardedRef,
() => {
return {
scrollToTop: (animated) => {
internalRef.current?.scrollToOffset({ animated, offset: 0 });
},
};
},
[]
);

const offsets = useCenteredTileOffsets({
containerWidth,
gap,
tileWidth,
tileCount: props.data?.length ?? 0,
containerHorizontalPadding,
alignment: snapToAlignment,
footerWidth,
});

const Gap = useMemo(() => {
return () => <View style={{ width: gap }} />;
}, [gap]);

return (
<AnimatedFlashList
ref={internalRef}
horizontal
disableHorizontalListHeightMeasurement
contentContainerStyle={{
paddingHorizontal: containerHorizontalPadding,
...contentContainerStyle,
}}
estimatedItemSize={props.data?.length === 1 ? 1 : tileWidth}
scrollIndicatorInsets={{
left: containerHorizontalPadding,
right: containerHorizontalPadding,
...scrollIndicatorInsets,
}}
ItemSeparatorComponent={Gap}
snapToOffsets={offsets}
showsHorizontalScrollIndicator={false}
scrollEventThrottle={8}
{...props}
/>
);
}
62 changes: 62 additions & 0 deletions template/src/components/kit/swiper/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useMemo } from 'react';
import { getAlignmentOffset } from './utils';

export interface UseTileOffsetsParams {
containerWidth: number;
containerHorizontalPadding: number;
tileWidth: number;
tileCount: number;
gap: number;
alignment: 'start' | 'center' | 'end';
footerWidth: number;
}

export function useCenteredTileOffsets({
containerWidth,
tileWidth,
tileCount,
containerHorizontalPadding,
gap,
alignment,
footerWidth,
}: UseTileOffsetsParams): number[] {
const paddedTileWidth = tileWidth + gap;
const alignmentOffset = getAlignmentOffset({
alignment,
containerWidth,
containerHorizontalPadding,
tileWidth,
});
const upperBound =
tileCount * tileWidth +
(tileCount - 1) * gap +
containerHorizontalPadding * 2 +
footerWidth -
containerWidth;

const offsets = useMemo(() => {
const allOffsets: number[] = [];

for (let i = 0; i < tileCount; i++) {
const startOfTile = i * paddedTileWidth + containerHorizontalPadding;
const offset = startOfTile - alignmentOffset;

if (offset >= upperBound) {
allOffsets.push(upperBound);
break;
}

allOffsets.push(offset);
}

return allOffsets;
}, [
tileCount,
upperBound,
alignmentOffset,
containerHorizontalPadding,
paddedTileWidth,
]);

return offsets;
}
1 change: 1 addition & 0 deletions template/src/components/kit/swiper/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Swiper';
22 changes: 22 additions & 0 deletions template/src/components/kit/swiper/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export interface GetAlignmentOffsetParams {
alignment: 'start' | 'center' | 'end';
containerWidth: number;
containerHorizontalPadding: number;
tileWidth: number;
}

export function getAlignmentOffset({
alignment,
containerWidth,
containerHorizontalPadding,
tileWidth,
}: GetAlignmentOffsetParams) {
switch (alignment) {
case 'start':
return containerHorizontalPadding;
case 'center':
return (containerWidth - tileWidth) / 2;
case 'end':
return containerWidth - (tileWidth + containerHorizontalPadding);
}
}
27 changes: 27 additions & 0 deletions template/src/screens/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SafeAreaView } from 'react-native-safe-area-context';
import { useTranslation } from 'react-i18next';
import { useCallback, useEffect, useState } from 'react';
import { useWindowDimensions } from 'react-native';
import { RootStackScreenProps } from '../navigation/RootNavigator';
import { ActivityIndicator, Button, Flex } from '../components/kit';
import { useToaster, Text } from '~/components/kit';
Expand All @@ -12,12 +13,14 @@ import Geolocation, {
import { useApplicationConfiguration } from '~/hooks/use-application-configuration';
import { useRemoteConfig } from '~/hooks/use-remote-config';
import { useNewFeature } from '~/hooks/use-new-feature';
import { Swiper } from '~/components/kit/swiper';

export type HomeScreenProps = RootStackScreenProps<'Home'>;

export function HomeScreen({ navigation }: HomeScreenProps) {
const geolocation = useService(Geolocation);
const { i18n } = useTranslation();
const { width } = useWindowDimensions();

const apiUrl = useApplicationConfiguration('API_URL');
const secretPanelEnabled = useApplicationConfiguration(
Expand Down Expand Up @@ -110,6 +113,30 @@ export function HomeScreen({ navigation }: HomeScreenProps) {
</Flex>
)}
</Flex>

<Swiper
containerWidth={width}
estimatedListSize={{ width, height: 250 }}
containerHorizontalPadding={24}
data={[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}
gap={16}
tileWidth={200}
snapToAlignment="start"
renderItem={({ item }) => (
<Flex
width={200}
height={250}
background="hotpink"
radius={12}
align="center"
justify="center"
>
<Text size={16} weight="700" centered>
{item}
</Text>
</Flex>
)}
/>
</SafeAreaView>
);
}
31 changes: 29 additions & 2 deletions template/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2185,6 +2185,14 @@
dependencies:
nanoid "^3.1.23"

"@shopify/flash-list@^1.7.1":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@shopify/flash-list/-/flash-list-1.7.1.tgz#11644551d86b8a9ef83f521487bebba478bfc011"
integrity sha512-sUYl7h8ydJutufA26E42Hj7cLvaBTpkMIyNJiFrxUspkcANb6jnFiLt9rEwAuDjvGk/C0lHau+WyT6ZOxqVPwg==
dependencies:
recyclerlistview "4.2.1"
tslib "2.6.3"

"@sideway/address@^4.1.3":
version "4.1.4"
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0"
Expand Down Expand Up @@ -5543,7 +5551,7 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"

lodash.debounce@^4.0.8:
lodash.debounce@4.0.8, lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
Expand Down Expand Up @@ -6472,7 +6480,7 @@ prompts@^2.0.1, prompts@^2.4.2:
kleur "^3.0.3"
sisteransi "^1.0.5"

prop-types@^15.7.2, prop-types@^15.8.1:
prop-types@15.8.1, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
Expand Down Expand Up @@ -6797,6 +6805,15 @@ recast@^0.21.0:
source-map "~0.6.1"
tslib "^2.0.1"

[email protected]:
version "4.2.1"
resolved "https://registry.yarnpkg.com/recyclerlistview/-/recyclerlistview-4.2.1.tgz#4537a0959400cdce1df1f38d26aab823786e9b13"
integrity sha512-NtVYjofwgUCt1rEsTp6jHQg/47TWjnO92TU2kTVgJ9wsc/ely4HnizHHa+f/dI7qaw4+zcSogElrLjhMltN2/g==
dependencies:
lodash.debounce "4.0.8"
prop-types "15.8.1"
ts-object-utils "0.0.5"

redent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
Expand Down Expand Up @@ -7535,6 +7552,11 @@ ts-node@^10.9.1:
v8-compile-cache-lib "^3.0.1"
yn "3.1.1"

[email protected]:
version "0.0.5"
resolved "https://registry.yarnpkg.com/ts-object-utils/-/ts-object-utils-0.0.5.tgz#95361cdecd7e52167cfc5e634c76345e90a26077"
integrity sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA==

tsconfig-paths@^3.14.2:
version "3.14.2"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088"
Expand All @@ -7545,6 +7567,11 @@ tsconfig-paths@^3.14.2:
minimist "^1.2.6"
strip-bom "^3.0.0"

[email protected]:
version "2.6.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==

tslib@^1.8.1, tslib@^1.9.3:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
Expand Down

0 comments on commit 7ed41cc

Please sign in to comment.