Replace FlatList with Carousel

This commit is contained in:
Johannes Klein
2026-06-16 11:25:16 +02:00
parent f636415141
commit d85f93bfd5
2 changed files with 38 additions and 74 deletions

View File

@@ -4,15 +4,15 @@ import {
} from "components/SharedComponents";
import { View } from "components/styledComponents";
import React, {
useCallback, useMemo, useState,
useMemo, useState,
} from "react";
import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native";
import { FlatList } from "react-native";
import {
Gesture,
GestureDetector,
GestureHandlerRootView,
} from "react-native-gesture-handler";
import type { CarouselRenderItem, ICarouselInstance } from "react-native-reanimated-carousel";
import Carousel from "react-native-reanimated-carousel";
import Photo from "realmModels/Photo";
import useDeviceOrientation from "sharedHooks/useDeviceOrientation";
import useTranslation from "sharedHooks/useTranslation";
@@ -37,7 +37,7 @@ interface SoundItem {
interface Props {
autoPlaySound?: boolean; // automatically start playing a sound when it is visible
editable?: boolean;
horizontalScroll: React.Ref<FlatList>;
horizontalScroll: React.Ref<ICarouselInstance>;
onDeletePhoto: ( uri: string ) => void;
onClose: ( ) => void;
onDeleteSound: ( uri: string ) => void;
@@ -63,19 +63,17 @@ const MainMediaDisplay = ( {
const { screenWidth } = useDeviceOrientation( );
const [displayHeight, setDisplayHeight] = useState( 0 );
const [zooming, setZooming] = useState( false );
const atFirstItem = selectedMediaIndex === 0;
const items = useMemo( ( ) => ( [
...photos.map( photo => ( { ...photo, type: "photo" as const } ) ),
...sounds.map( sound => ( { ...sound, type: "sound" as const } ) ),
] ), [photos, sounds] );
const atLastItem = selectedMediaIndex === items.length - 1;
// t changes a lot, but these strings don't, so using them as useCallback
// dependencies keeps that method from getting redefined a lot
const deletePhotoLabel = t( "Delete-photo" );
const deleteSoundLabel = t( "Delete-sound" );
const renderPhoto = useCallback( ( photo: PhotoItem ) => {
const renderPhoto = ( photo: PhotoItem ) => {
const uri = Photo.displayLocalOrRemoteLargePhoto( photo );
const hasAttribution = photo?.attribution;
return (
@@ -112,14 +110,9 @@ const MainMediaDisplay = ( {
}
</View>
);
}, [
deletePhotoLabel,
editable,
onDeletePhoto,
selectedMediaIndex,
] );
};
const renderSound = useCallback( ( sound: SoundItem ) => (
const renderSound = ( sound: SoundItem ) => (
<View
className="justify-center items-center"
style={{
@@ -145,65 +138,20 @@ const MainMediaDisplay = ( {
)
}
</View>
), [
autoPlaySound,
deleteSoundLabel,
displayHeight,
editable,
items,
onDeleteSound,
screenWidth,
selectedMediaIndex,
] );
);
const renderItem = useCallback( ( { item }: { item: PhotoItem | SoundItem } ) => (
const renderItem: CarouselRenderItem<PhotoItem | SoundItem> = ( { item } ) => (
item.type === "photo"
? renderPhoto( item )
: renderSound( item )
), [
renderPhoto,
renderSound,
] );
);
// need getItemLayout for setting initial scroll index
const getItemLayout = useCallback( ( data, idx: number ) => ( {
length: screenWidth,
offset: screenWidth * idx,
index: idx,
} ), [screenWidth] );
const handleScrollLeft = useCallback( ( index: number ) => {
if ( atFirstItem ) { return; }
setSelectedMediaIndex( index );
}, [atFirstItem, setSelectedMediaIndex] );
const handleScrollRight = useCallback( ( index: number ) => {
if ( atLastItem ) { return; }
setSelectedMediaIndex( index );
}, [atLastItem, setSelectedMediaIndex] );
const handleScrollEndDrag = useCallback( ( e: NativeSyntheticEvent<NativeScrollEvent> ) => {
const { contentOffset, layoutMeasurement } = e.nativeEvent;
const { x } = contentOffset;
const currentOffset = screenWidth * selectedMediaIndex;
// https://gist.github.com/dozsolti/6d01d0f96d9abced3450a2e6149a2bc3?permalink_comment_id=4107663#gistcomment-4107663
const index = Math.floor(
Math.floor( x ) / Math.floor( layoutMeasurement.width ),
);
if ( x > currentOffset ) {
handleScrollRight( index );
} else if ( x < currentOffset ) {
handleScrollLeft( index );
}
}, [
handleScrollLeft,
handleScrollRight,
screenWidth,
selectedMediaIndex,
] );
// // need getItemLayout for setting initial scroll index
// const getItemLayout = useCallback( ( data, idx: number ) => ( {
// length: screenWidth,
// offset: screenWidth * idx,
// index: idx,
// } ), [screenWidth] );
const swipeToCloseGesture = Gesture.Simultaneous(
Gesture.Pan( )
@@ -227,7 +175,22 @@ const MainMediaDisplay = ( {
>
<GestureHandlerRootView>
<GestureDetector gesture={swipeToCloseGesture}>
<FlatList
<View collapsable={false}>
<Carousel
key={`MediaViewerCarousel-${screenWidth}`}
testID="MediaViewer.carousel"
ref={horizontalScroll}
data={items}
renderItem={renderItem}
defaultIndex={selectedMediaIndex}
loop={false}
width={screenWidth}
enabled={!zooming}
onSnapToItem={setSelectedMediaIndex}
// onConfigurePanGesture={onConfigurePanGesture}
// windowSize={3}
/>
{/* <FlatList
ref={horizontalScroll}
data={items}
renderItem={renderItem}
@@ -239,7 +202,8 @@ const MainMediaDisplay = ( {
scrollEnabled={!zooming}
showsHorizontalScrollIndicator={false}
onMomentumScrollEnd={handleScrollEndDrag}
/>
/> */}
</View>
</GestureDetector>
</GestureHandlerRootView>
</View>

View File

@@ -7,8 +7,8 @@ import React, {
useRef,
useState,
} from "react";
import type { FlatList } from "react-native";
import { StatusBar } from "react-native";
import type { ICarouselInstance } from "react-native-reanimated-carousel";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Photo from "realmModels/Photo";
import { BREAKPOINTS } from "sharedHelpers/breakpoint";
@@ -78,7 +78,7 @@ const MediaViewer = ( {
const { t } = useTranslation( );
const [mediaToDelete, setMediaToDelete] = useState<MediaToDelete | null>( null );
const horizontalScroll = useRef<FlatList>( null );
const horizontalScroll = useRef<ICarouselInstance>( null );
const { screenWidth } = useDeviceOrientation( );
const isLargeScreen = screenWidth > BREAKPOINTS.md;
@@ -87,7 +87,7 @@ const MediaViewer = ( {
// when a user taps an item in the carousel, the UI needs to automatically
// scroll to the index of the item they selected
setSelectedMediaIndex( index );
horizontalScroll?.current?.scrollToIndex( { index, animated: true } );
horizontalScroll?.current?.scrollTo( { index, animated: true } );
}, [setSelectedMediaIndex] );
// If we've removed an item the selectedPhoto index might refer to a item
@@ -96,7 +96,7 @@ const MediaViewer = ( {
if ( uris.length > 0 && selectedMediaIndex >= uris.length ) {
const newIndex = Math.max( 0, selectedMediaIndex - 1 );
setSelectedMediaIndex( newIndex );
horizontalScroll?.current?.scrollToIndex( {
horizontalScroll?.current?.scrollTo( {
index: newIndex,
animated: false,
} );