Skip to content

Commit

Permalink
sync with scroll
Browse files Browse the repository at this point in the history
  • Loading branch information
jakzaizzat committed Apr 22, 2024
1 parent 4cb85fb commit 4804304
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 21 deletions.
55 changes: 41 additions & 14 deletions apps/mobile/src/components/GalleryEditor/GalleryEditorRender.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import { FlashList, ListRenderItem } from '@shopify/flash-list';
import { useCallback, useEffect, useMemo } from 'react';
import { View } from 'react-native';
import { NativeScrollEvent, NativeSyntheticEvent, View } from 'react-native';
import { useAnimatedRef, useSharedValue } from 'react-native-reanimated';
import { graphql, useFragment } from 'react-relay';

import { useGalleryEditorActions } from '~/contexts/GalleryEditor/GalleryEditorContext';
Expand All @@ -17,7 +18,7 @@ import { GalleryEditorHeader } from './GalleryEditorHeader';
import { GalleryEditorNavbar } from './GalleryEditorNavbar';
import { GalleryEditorSection } from './GalleryEditorSection';

type ListItemType =
export type ListItemType =
| { kind: 'navigation'; title: string }
| { kind: 'header'; galleryRef: GalleryEditorHeaderFragment$key }
| { kind: 'section'; section: StagedSection; queryRef: GalleryEditorSectionFragment$key };
Expand Down Expand Up @@ -86,17 +87,37 @@ export function GalleryEditorRender({ galleryRef, queryRef }: Props) {
return items;
}, [gallery, sections, query]);

const renderItem = useCallback<ListRenderItem<ListItemType>>(({ item }) => {
if (item.kind === 'header') {
return <GalleryEditorHeader galleryRef={item.galleryRef} />;
} else if (item.kind === 'navigation') {
return <GalleryEditorNavbar />;
} else if (item.kind === 'section') {
return <GalleryEditorSection section={item.section} queryRef={item.queryRef} />;
} else {
return null;
}
}, []);
const scrollContentOffsetY = useSharedValue(0);
const ref = useAnimatedRef<FlashList<ListItemType>>();

const renderItem = useCallback<ListRenderItem<ListItemType>>(
({ item }) => {
if (item.kind === 'header') {
return <GalleryEditorHeader galleryRef={item.galleryRef} />;
} else if (item.kind === 'navigation') {
return <GalleryEditorNavbar />;
} else if (item.kind === 'section') {
return (
<GalleryEditorSection
section={item.section}
queryRef={item.queryRef}
scrollContentOffsetY={scrollContentOffsetY}
scrollViewRef={ref}
/>
);
} else {
return null;
}
},
[ref, scrollContentOffsetY]
);

const handleScroll = useCallback(
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
scrollContentOffsetY.value = e.nativeEvent.contentOffset.y;
},
[scrollContentOffsetY]
);

return (
<View
Expand All @@ -105,7 +126,13 @@ export function GalleryEditorRender({ galleryRef, queryRef }: Props) {
paddingTop: top,
}}
>
<FlashList data={items} renderItem={renderItem} estimatedItemSize={93} />
<FlashList
ref={ref}
data={items}
renderItem={renderItem}
estimatedItemSize={93}
onScroll={handleScroll}
/>
</View>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { FlashList } from '@shopify/flash-list';
import clsx from 'clsx';
import { useCallback, useMemo } from 'react';
import { View } from 'react-native';
import { AnimatedRef, SharedValue } from 'react-native-reanimated';
import { graphql, useFragment } from 'react-relay';
import { DragIcon } from 'src/icons/DragIcon';

Expand All @@ -11,14 +13,23 @@ import { GalleryEditorSectionFragment$key } from '~/generated/GalleryEditorSecti
import { GalleryTouchableOpacity } from '../GalleryTouchableOpacity';
import ProcessedText from '../ProcessedText/ProcessedText';
import { BaseM } from '../Text';
import { ListItemType } from './GalleryEditorRender';
import { SortableRowList } from './SortableRowList';

type Props = {
section: StagedSection;
queryRef: GalleryEditorSectionFragment$key;

scrollContentOffsetY: SharedValue<number>;
scrollViewRef: AnimatedRef<FlashList<ListItemType>>;
};

export function GalleryEditorSection({ section, queryRef }: Props) {
export function GalleryEditorSection({
section,
queryRef,
scrollContentOffsetY,
scrollViewRef,
}: Props) {
const query = useFragment(
graphql`
fragment GalleryEditorSectionFragment on Query {
Expand Down Expand Up @@ -76,6 +87,8 @@ export function GalleryEditorSection({ section, queryRef }: Props) {
sectionId={section.dbid}
queryRef={query}
onDragEnd={handleDragEnd}
scrollContentOffsetY={scrollContentOffsetY}
scrollViewRef={scrollViewRef}
/>
</View>
</GalleryTouchableOpacity>
Expand Down
56 changes: 52 additions & 4 deletions apps/mobile/src/components/GalleryEditor/SortableRow.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { FlashList } from '@shopify/flash-list';
import { useCallback } from 'react';
import { useWindowDimensions } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { trigger } from 'react-native-haptic-feedback';
import Animated, {
AnimatedRef,
runOnJS,
scrollTo,
SharedValue,
useAnimatedReaction,
useAnimatedStyle,
Expand All @@ -11,7 +15,9 @@ import Animated, {
withSpring,
withTiming,
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { ListItemType } from './GalleryEditorRender';
import { ItemHeights, Positions } from './SortableRowList';

type Props = {
Expand All @@ -20,6 +26,8 @@ type Props = {
positions: SharedValue<Positions>;
animatedIndex: SharedValue<number | null>;
itemHeights: SharedValue<ItemHeights>;
scrollContentOffsetY: SharedValue<number>;
scrollViewRef: AnimatedRef<FlashList<ListItemType>>;

onDragEnd: (data: string[]) => void;
};
Expand All @@ -30,6 +38,8 @@ export function SortableRow({
positions,
animatedIndex,
itemHeights,
scrollContentOffsetY,
scrollViewRef,
onDragEnd,
}: Props) {
const itemHeight = itemHeights.value[index] ?? 0;
Expand Down Expand Up @@ -93,6 +103,36 @@ export function SortableRow({
[positions, itemHeights]
);

// Get safe area insets and window dimensions
const inset = useSafeAreaInsets();
const { height: windowHeight } = useWindowDimensions();
const containerHeight = windowHeight - inset.top;

// Callback to handle edge cases while scrolling
// (when the user drags the item to the top or bottom of the list)
// Since we need to update the scroll position of the scroll view (scrollTo)
const scrollLogic = useCallback(
({ absoluteY }: { absoluteY: number }) => {
'worklet';
const lowerBound = 1.5 * itemHeight;
const upperBound = scrollContentOffsetY.value + containerHeight;

// scroll speed is proportional to the item height (the bigger the item, the faster it scrolls)
const scrollSpeed = itemHeight * 0.1;

if (absoluteY <= lowerBound) {
// while scrolling to the top of the list
const nextPosition = scrollContentOffsetY.value - scrollSpeed;
scrollTo(scrollViewRef, 0, Math.max(nextPosition, 0), false);
} else if (absoluteY + scrollContentOffsetY.value >= upperBound) {
// while scrolling to the bottom of the list
const nextPosition = scrollContentOffsetY.value + scrollSpeed;
scrollTo(scrollViewRef, 0, Math.max(nextPosition, 0), false);
}
},
[containerHeight, itemHeight, scrollContentOffsetY.value, scrollViewRef]
);

// Need to keep track of the previous positions to check if the positions have changed
// This is needed to trigger the onDragEnd callback
const prevPositions = useSharedValue({});
Expand All @@ -104,17 +144,25 @@ export function SortableRow({
prevPositions.value = Object.assign({}, positions.value);

animatedIndex.value = index;

contextY.value = positions.value[index] ?? 0;
// Keep the reference of the initialContentOffset
// But that's extremely important to handle the edge cases while scrolling
// Notice:
// 1. In the context we subtract the scrollContentOffsetY.value
// 2. In the onUpdate we add the scrollContentOffsetY.value
// In the common case the contribution of the scrollContentOffsetY.value will be 0
// But in the edge cases the scrollContentOffsetY.value will be updated during the onUpdate
contextY.value = (positions.value[index] ?? 0) - scrollContentOffsetY.value;

translateX.value = event.translationX;

runOnJS(trigger)('impactLight');
})
.onUpdate((event) => {
const { translationY } = event;
const { absoluteY, translationY } = event;

const translateY = contextY.value + translationY + scrollContentOffsetY.value;

const translateY = contextY.value + translationY;
scrollLogic({ absoluteY });

for (let i = 0; i < Object.keys(positions.value).length; i++) {
// Check if the translateY is in range of another item
Expand Down
18 changes: 16 additions & 2 deletions apps/mobile/src/components/GalleryEditor/SortableRowList.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { FlashList } from '@shopify/flash-list';
import { useEffect, useMemo } from 'react';
import { useWindowDimensions, View } from 'react-native';
import { useSharedValue } from 'react-native-reanimated';
import { AnimatedRef, SharedValue, useSharedValue } from 'react-native-reanimated';
import { graphql, useFragment } from 'react-relay';

import { StagedRowList } from '~/contexts/GalleryEditor/types';
import { SortableRowListFragment$key } from '~/generated/SortableRowListFragment.graphql';

import { ListItemType } from './GalleryEditorRender';
import { GalleryEditorRow } from './GalleryEditorRow';
import { SortableRow } from './SortableRow';
import { calculateItemHeights, calculateOffsetsRow, calculatePositions } from './utils';
Expand All @@ -17,6 +19,9 @@ type Props = {
queryRef: SortableRowListFragment$key;

onDragEnd: (data: string[]) => void;

scrollContentOffsetY: SharedValue<number>;
scrollViewRef: AnimatedRef<FlashList<ListItemType>>;
};

type Index = number;
Expand All @@ -26,7 +31,14 @@ type HeightValue = number;
export type Positions = Record<Index, PositionValue>;
export type ItemHeights = Record<Index, HeightValue>;

export function SortableRowList({ rows, sectionId, queryRef, onDragEnd }: Props) {
export function SortableRowList({
rows,
sectionId,
queryRef,
onDragEnd,
scrollContentOffsetY,
scrollViewRef,
}: Props) {
const query = useFragment(
graphql`
fragment SortableRowListFragment on Query {
Expand Down Expand Up @@ -80,6 +92,8 @@ export function SortableRowList({ rows, sectionId, queryRef, onDragEnd }: Props)
positions={positions}
animatedIndex={animatedIndex}
itemHeights={itemHeights}
scrollContentOffsetY={scrollContentOffsetY}
scrollViewRef={scrollViewRef}
onDragEnd={onDragEnd}
>
<GalleryEditorRow sectionId={sectionId} row={row} queryRef={query} />
Expand Down

0 comments on commit 4804304

Please sign in to comment.