Skip to content

Commit

Permalink
🛠️ Fix carousel scrolling in AttachmentCarousel
Browse files Browse the repository at this point in the history
- Fixed an issue with carousel scrolling in the AttachmentCarousel component
- Implemented a new pan gesture for smoother scrolling experience
- Adjusted layout calculations for optimal rendering of attachments

Related to:
- Expensify#23546
- Expensify#39833
- Expensify#22318
- Expensify#21177
- Expensify#31166
  • Loading branch information
kidroca committed Apr 22, 2024
1 parent 448982d commit 0887405
Showing 1 changed file with 55 additions and 37 deletions.
92 changes: 55 additions & 37 deletions src/components/Attachments/AttachmentCarousel/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import isEqual from 'lodash/isEqual';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import type {ListRenderItemInfo} from 'react-native';
import {FlatList, Keyboard, PixelRatio, View} from 'react-native';
import {Keyboard, PixelRatio, View} from 'react-native';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import Animated, {scrollTo, useAnimatedRef} from 'react-native-reanimated';
import {withOnyx} from 'react-native-onyx';
import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import BlockingView from '@components/BlockingViews/BlockingView';
Expand All @@ -15,7 +17,7 @@ import Navigation from '@libs/Navigation/Navigation';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import AttachmentCarouselCellRenderer from './AttachmentCarouselCellRenderer';
import useWindowDimensions from '@hooks/useWindowDimensions';
import CarouselActions from './CarouselActions';
import CarouselButtons from './CarouselButtons';
import CarouselItem from './CarouselItem';
Expand All @@ -29,16 +31,21 @@ const viewabilityConfig = {
itemVisiblePercentThreshold: 95,
};

const MIN_FLING_VELOCITY = 500;

function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility}: AttachmentCarouselProps) {
const theme = useTheme();
const {translate} = useLocalize();
const {isSmallScreenWidth, windowWidth} = useWindowDimensions();
const styles = useThemeStyles();
const {isFullScreenRef} = useFullScreenContext();
const scrollRef = useRef<FlatList>(null);
const scrollRef = useAnimatedRef<Animated.FlatList<ListRenderItemInfo<Attachment>>>();

const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();

const [containerWidth, setContainerWidth] = useState(0);
const modalStyles = styles.centeredModalStyles(isSmallScreenWidth, true);
const cellWidth = useMemo(() => PixelRatio.roundToNearestPixel(windowWidth - (modalStyles.marginHorizontal + modalStyles.borderWidth) * 2), [windowWidth]);

const [page, setPage] = useState(0);
const [attachments, setAttachments] = useState<Attachment[]>([]);
const [activeSource, setActiveSource] = useState<AttachmentSource | null>(source);
Expand Down Expand Up @@ -120,7 +127,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,

scrollRef.current.scrollToIndex({index: nextIndex, animated: canUseTouchScreen});
},
[attachments, canUseTouchScreen, isFullScreenRef, page],
[attachments, canUseTouchScreen, isFullScreenRef, page, scrollRef],
);

const extractItemKey = useCallback(
Expand All @@ -132,35 +139,56 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
/** Calculate items layout information to optimize scrolling performance */
const getItemLayout = useCallback(
(data: ArrayLike<Attachment> | null | undefined, index: number) => ({
length: containerWidth,
offset: containerWidth * index,
length: cellWidth,
offset: cellWidth * index,
index,
}),
[containerWidth],
[cellWidth],
);

/** Defines how a single attachment should be rendered */
const renderItem = useCallback(
({item}: ListRenderItemInfo<Attachment>) => (
<CarouselItem
item={item}
isFocused={activeSource === item.source}
onPress={canUseTouchScreen ? () => setShouldShowArrows((oldState: boolean) => !oldState) : undefined}
isModalHovered={shouldShowArrows}
/>
<View style={[styles.h100, {width: cellWidth}]}>
<CarouselItem
item={item}
isFocused={activeSource === item.source}
onPress={canUseTouchScreen ? () => setShouldShowArrows((oldState) => !oldState) : undefined}
isModalHovered={shouldShowArrows}
/>
</View>
),
[activeSource, canUseTouchScreen, setShouldShowArrows, shouldShowArrows],
[activeSource, canUseTouchScreen, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100],
);

/** Pan gesture handing swiping through attachments on touch screen devices */
const pan = useMemo(
() =>
Gesture.Pan()
.enabled(canUseTouchScreen)
.onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX, 0, false))
.onEnd(({translationX, velocityX}) => {
let newIndex;
if (velocityX > MIN_FLING_VELOCITY) {
// User flung to the right
newIndex = Math.max(0, page - 1);
} else if (velocityX < -MIN_FLING_VELOCITY) {
// User flung to the left
newIndex = Math.min(attachments.length - 1, page + 1);
} else {
// snap scroll position to the nearest cell (making sure it's within the bounds of the list)
const delta = Math.round(-translationX / cellWidth)
newIndex = Math.min(attachments.length - 1, Math.max(0, page + delta));
}

scrollTo(scrollRef, newIndex * cellWidth, 0, true);
}),
[attachments.length, canUseTouchScreen, cellWidth, page, scrollRef],
);

return (
<View
style={[styles.flex1, styles.attachmentCarouselContainer]}
onLayout={({nativeEvent}) => {
if (isFullScreenRef.current) {
return;
}
setContainerWidth(PixelRatio.roundToNearestPixel(nativeEvent.layout.width));
}}
onMouseEnter={() => !canUseTouchScreen && setShouldShowArrows(true)}
onMouseLeave={() => !canUseTouchScreen && setShouldShowArrows(false)}
>
Expand All @@ -184,36 +212,26 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
cancelAutoHideArrow={cancelAutoHideArrows}
/>

{containerWidth > 0 && (
<FlatList
<GestureDetector gesture={pan}>
<Animated.FlatList
keyboardShouldPersistTaps="handled"
horizontal
decelerationRate="fast"
showsHorizontalScrollIndicator={false}
bounces={false}
// Scroll only one image at a time no matter how fast the user swipes
disableIntervalMomentum
pagingEnabled
snapToAlignment="start"
snapToInterval={containerWidth}
// Enable scrolling by swiping on mobile (touch) devices only
// disable scroll for desktop/browsers because they add their scrollbars
// Enable scrolling FlatList only when PDF is not in a zoomed state
scrollEnabled={canUseTouchScreen}
// scrolling is controlled by the pan gesture
scrollEnabled={false}
ref={scrollRef}
initialScrollIndex={page}
initialNumToRender={3}
windowSize={5}
maxToRenderPerBatch={CONST.MAX_TO_RENDER_PER_BATCH.CAROUSEL}
data={attachments}
CellRendererComponent={AttachmentCarouselCellRenderer}
renderItem={renderItem}
getItemLayout={getItemLayout}
keyExtractor={extractItemKey}
viewabilityConfig={viewabilityConfig}
onViewableItemsChanged={updatePage}
/>
)}
</GestureDetector>

<CarouselActions onCycleThroughAttachments={cycleThroughAttachments} />
</>
Expand Down

0 comments on commit 0887405

Please sign in to comment.