react-native-reanimated-image-viewer
react-native-reanimated-image-viewer copied to clipboard
How about some features?
Hi! How about array of images (imageUrls)? Something like react-native-image-viewing.
This is definitely something I wanna add, but since it would involves the FlatList, the gestures would be a bit more complex, and I haven't studied how I would do this yet
I think you can give FlatList or ScrollView scrollEnabled={false}, and give the listRef as props to ImageViewer. And then, you can scroll List by scrollTo (from reanimated) with gesture handler. List:
<ScrollView
ref={listRef}
scrollEnabled={false}
pagingEnabled
horizontal
showsHorizontalScrollIndicator={false}>
{images.map((image, index) => (
<ImageViewer
listRef={listRef}
index={index}
{...image}
key={index}
maxIndex={images.length - 1}
onDismiss={handleDismiss}
/>
))}
</ScrollView>
ImageViewer:
onActive: () => {
scrollTo(listRef, index * SCREEN_WIDTH - overOffsetX, 0, animated: false)
},
onEnd: () => {
if (Math.abs(overOffsetX) >= HORIZONTAL_THRESHOLD) {
overOffsetX > 0
? scrollTo(listRef, (index - 1) * SCREEN_WIDTH, 0, true)
: scrollTo(listRef, (index + 1) * SCREEN_WIDTH, 0, true);
} else {
scrollTo(listRef, index * SCREEN_WIDTH, 0, true);
}
}
I think you can give FlatList or ScrollView scrollEnabled={false}, and give the listRef as props to ImageViewer. And then, you can scroll List by scrollTo (from reanimated) with gesture handler. List:
<ScrollView ref={listRef} scrollEnabled={false} pagingEnabled horizontal showsHorizontalScrollIndicator={false}> {images.map((image, index) => ( <ImageViewer listRef={listRef} index={index} {...image} key={index} maxIndex={images.length - 1} onDismiss={handleDismiss} /> ))} </ScrollView>ImageViewer:
onActive: () => { scrollTo(listRef, index * SCREEN_WIDTH - overOffsetX, 0, animated: false) }, onEnd: () => { if (Math.abs(overOffsetX) >= HORIZONTAL_THRESHOLD) { overOffsetX > 0 ? scrollTo(listRef, (index - 1) * SCREEN_WIDTH, 0, true) : scrollTo(listRef, (index + 1) * SCREEN_WIDTH, 0, true); } else { scrollTo(listRef, index * SCREEN_WIDTH, 0, true); } }
@dodoto could you please explain in more detail the location of the code in ImageViewer?
a demo code with pan and tap scale
import React, { FC, useEffect, useRef } from 'react';
import { Dimensions, Image, StyleSheet, ImageURISource, Text, ScrollView, FlatList } from 'react-native';
import Animated, { useAnimatedStyle, useSharedValue, useAnimatedGestureHandler, withTiming, scrollTo, useAnimatedRef } from 'react-native-reanimated';
import { PanGestureHandler, PanGestureHandlerGestureEvent, TapGestureHandler, TapGestureHandlerGestureEvent } from 'react-native-gesture-handler';
const source = require('../assets/masthead.png');
const { width, height } = Dimensions.get('window'); // if statusbar translucent, use `Dimensions.get('screen')`
const center = { x: width / 2, y: height / 2 };
const getImageSize = (source: ImageURISource | number): Promise<{ width: number, height: number }> => {
return new Promise((resolve,reject) => {
if (typeof source === 'number') {
const { width, height } = Image.resolveAssetSource(source)
resolve({width, height})
} else {
Image.getSize(source.uri!, (width, height) => {
resolve({width, height})
},(error) => {
reject(error)
})
}
})
}
const styles = StyleSheet.create({
root: { flex: 1 },
container: {
width,
height,
position: "relative",
justifyContent: "center",
alignItems: 'center',
backgroundColor: "black"
},
image: {
width,
},
title: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
textAlign: 'center',
fontSize: 20,
fontWeight: 'bold',
color: 'white',
},
});
type PanGestureHandlerGestureContext = {
right: number;
left: number;
startTranslateX: number;
overOffsetX: number;
};
interface ImageViewerProps {
index: number;
title: string;
listRef: React.RefObject<ScrollView | FlatList>;
}
export const ImageViewer: FC<ImageViewerProps> = ({ index, title, listRef }) => {
const imageSize = useSharedValue({ width, height: 0 });
const imageTranslate = useSharedValue({ x: 0, y: 0 });
const imageScale = useSharedValue(1);
const tap = useRef();
const pan = useRef();
const handleTap = useAnimatedGestureHandler<TapGestureHandlerGestureEvent>({
onEnd() {
const isScaled = imageScale.value === 2;
imageScale.value = withTiming(isScaled ? 1 : 2);
if (isScaled) {
imageTranslate.value.x = withTiming(0);
}
}
});
// only handle horizontal
const handlePan = useAnimatedGestureHandler<PanGestureHandlerGestureEvent, PanGestureHandlerGestureContext>({
onStart(_event, context) {
console.log('title', title);
const leftBound = Math.min(center.x - (imageSize.value.width * imageScale.value) / 2, 0); // ---->
const rightBound = -leftBound; // <------
context.left = leftBound;
context.right = rightBound;
context.startTranslateX = imageTranslate.value.x;
},
onActive(event, context) {
const rawTranslate = event.translationX + context.startTranslateX;
let translateX = rawTranslate;
let overOffsetX = 0;
// trigger list slide
if (translateX > context.right) {
translateX = context.right;
overOffsetX = rawTranslate - context.right;
console.log('slide to right');
}
if (translateX < context.left) {
translateX = context.left;
overOffsetX = rawTranslate - context.left;
console.log('slide to left');
}
context.overOffsetX = overOffsetX;
if (overOffsetX !== 0) {
scrollTo(listRef, -overOffsetX + index * width, 0, false);
}
const translateY = imageTranslate.value.y;
imageTranslate.value = {
x: translateX,
y: translateY,
};
},
onEnd(_event, context) {
const shreshold = 100;
let scrollX = index * width;
if (context.overOffsetX >= shreshold) {
scrollX = (index - 1) * width;
}
if (context.overOffsetX <= -shreshold) {
scrollX = (index + 1) * width;
}
scrollTo(listRef, scrollX, 0, true);
},
});
const imageStyle = useAnimatedStyle(() => ({
width: imageSize.value.width,
height: imageSize.value.height,
transform: [
{ translateX: imageTranslate.value.x },
{ translateY: imageTranslate.value.y },
{ scale: imageScale.value },
],
}))
useEffect(() => {
getImageSize(source).then((size) => {
imageSize.value = {
width,
height: width * size.height / size.width,
};
})
}, []);
return (
<Animated.View style={styles.container}>
<TapGestureHandler ref={tap} simultaneousHandlers={[pan]} maxDist={4} onGestureEvent={handleTap}>
<Animated.View>
<PanGestureHandler ref={pan} simultaneousHandlers={[tap]} onGestureEvent={handlePan}>
<Animated.Image
source={source}
resizeMode="stretch"
style={imageStyle}/>
</PanGestureHandler>
</Animated.View>
</TapGestureHandler>
<Text style={styles.title}>{ title }</Text>
</Animated.View>
);
};
const ImageList: FC = () => {
const listRef = useAnimatedRef<ScrollView>();
return (
<ScrollView
ref={listRef}
style={styles.root}
scrollEnabled={false}
horizontal>
<ImageViewer title="1" listRef={listRef} index={0}/>
<ImageViewer title="2" listRef={listRef} index={1}/>
</ScrollView>
);
}
export default ImageList;