Skip to content

Instantly share code, notes, and snippets.

@hungtrn75
Created July 14, 2021 07:34
Show Gist options
  • Save hungtrn75/fff763deee0d22ee13cc6a3eccdf5509 to your computer and use it in GitHub Desktop.
Save hungtrn75/fff763deee0d22ee13cc6a3eccdf5509 to your computer and use it in GitHub Desktop.
ImageViewer
import { dimensions } from "@constants/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { ActivityIndicator, Image, Platform, Pressable, StatusBar, StyleSheet, Text, View } from "react-native";
import FastImage from "react-native-fast-image";
import { FlatList, PanGestureHandler, PinchGestureHandler } from "react-native-gesture-handler";
import Animated, {
cancelAnimation,
runOnUI,
scrollTo,
useAnimatedGestureHandler,
useAnimatedRef,
useAnimatedScrollHandler,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withSpring,
} from "react-native-reanimated";
import { clamp, ReText, useVector } from "react-native-redash";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
const clampIndex = (val, min, max) => Math.min(Math.max(val, min), max);
const ImageViewerFooterItem = React.memo(({ item, aIndex, index, onPress }) => {
const style = useAnimatedStyle(() => {
return {
borderColor: aIndex.value - 1 == index ? "orange" : "gray",
};
});
return (
<Pressable onPress={onPress(index)}>
<Animated.View style={[styles.r, style]}>
<FastImage source={item.image} style={styles.fItem} />
</Animated.View>
</Pressable>
);
});
const ImageViewerMainItem = React.memo(
({ item, imageSizes, vec, scale, index }) => {
const pan = useRef();
const pinch = useRef();
const _onPanHandlerStateChange = useAnimatedGestureHandler({
onStart: (_, ctx) => {
cancelAnimation(vec.x);
cancelAnimation(vec.y);
ctx.x = vec.x.value;
ctx.y = vec.y.value;
},
onActive: ({ translationX, translationY }, ctx) => {
vec.x.value = translationX / scale.value + ctx.x;
vec.y.value = translationY / scale.value + ctx.y;
},
onFinish: () => {
},
});
const _onPinchHandlerStateChange = useAnimatedGestureHandler({
onStart: (_, ctx) => {
cancelAnimation(scale);
ctx.scale = scale.value;
},
onActive: ({ scale: s }, ctx) => {
scale.value = ctx.scale * s;
},
onFinish: (_, __) => {
if (scale.value < 1) {
scale.value = withSpring(1);
vec.x.value = withSpring(0);
vec.y.value = withSpring(0);
}
},
});
const style = useAnimatedStyle(() => {
return {
transform: [
{ perspective: 200 },
{ scale: scale.value },
{ translateX: vec.x.value },
{ translateY: vec.y.value },
],
};
});
return (
<PanGestureHandler
ref={pan}
simultaneousHandlers={[pinch]}
onHandlerStateChange={_onPanHandlerStateChange}
minDist={10}
minPointers={2}
maxPointers={2}
avgTouches>
<Animated.View style={styles.qq}>
<PinchGestureHandler
ref={pinch}
onHandlerStateChange={_onPinchHandlerStateChange}>
<Animated.View style={[styles.t, style]}>
<FastImage
source={item.image}
style={{
width: dimensions.SCREEN_WIDTH,
height:
imageSizes.width && imageSizes.height
? (dimensions.SCREEN_WIDTH * imageSizes.height) /
imageSizes.width
: 0,
}}
/>
</Animated.View>
</PinchGestureHandler>
</Animated.View>
</PanGestureHandler>
);
},
);
const initValues = len =>
Array(len)
.fill(0)
.map(() => {
const nV = useVector(0, 0);
const nS = useSharedValue(1);
return {
scale: nS,
vec: nV,
};
});
const ImageViewer = ({ navigation, route }) => {
const { title, activeIndex, imgs } = route.params || {
title: "Hình ảnh",
activeIndex: 0,
imgs: [],
};
const images = [imgs[imgs.length - 1], ...imgs, imgs[0]];
const [render, setRender] = useState(true);
const inset = useSafeAreaInsets();
const aref = useAnimatedRef();
const fScroll = useAnimatedRef();
const imageSizes = useRef({});
const aIndex = useSharedValue(
clampIndex(activeIndex + 1, 1, images.length - 2),
);
const mIndex = useSharedValue(
clampIndex(activeIndex + 1, 1, images.length - 2),
);
const scrollOffset = useSharedValue(0);
const imgLen = useSharedValue(images.length);
const dimenAni = useVector(dimensions.SCREEN_WIDTH, dimensions.SCREEN_HEIGHT);
const animatedRefs = useRef([...initValues(images.length)]);
const indiText = useDerivedValue(() => {
return `${aIndex.value} / ${imgLen.value - 2}`;
}, [aIndex, imgLen]);
useEffect(() => {
loadImage(0);
runOnUI(scrollFooter)(false);
}, []);
const onClose = useCallback(navigation.goBack, [navigation]);
const fItemPress = index => () => {
"worklet";
aref.current?.scrollToIndex({
animated: true,
index: index + 1,
});
if (Platform.OS == "android") {
const x = dimenAni.x.value * (index + 1);
const fW = dimenAni.x.value;
const fI = (x / fW).toFixed();
const cIndex = Math.min(Math.max(fI, 0), imgLen.value);
mIndex.value = aIndex.value;
if (cIndex < 1) {
aIndex.value = imgLen.value - 2;
scrollTo(aref, aIndex.value * fW, 0, false);
} else if (cIndex >= imgLen.value - 1) {
aIndex.value = 1;
scrollTo(aref, fW, 0, false);
} else {
aIndex.value = cIndex;
}
scrollFooter();
if (mIndex.value != aIndex.value) {
animatedRefs.current[mIndex.value].vec.x.value = 0;
animatedRefs.current[mIndex.value].vec.y.value = 0;
animatedRefs.current[mIndex.value].scale.value = 1;
}
}
};
const renderItem = ({ item, index }) => (
<ImageViewerFooterItem
key={index}
item={item}
aIndex={aIndex}
index={index}
onPress={fItemPress}
/>
);
const renderMainItem = ({ item, index }) => {
return (
<ImageViewerMainItem
key={index}
item={item}
imageSizes={imageSizes.current[index] ?? { width: 0, height: 0 }}
index={index}
vec={animatedRefs.current[index].vec}
scale={animatedRefs.current[index].scale}
/>
);
};
const continuous = index => {
const len = Object.values(imageSizes.current).length;
if (len === images.length) {
setRender(!render);
}
loadImage(index);
};
const clampHeight = h => {
let fH = dimensions.SCREEN_HEIGHT - inset.top - inset.bottom - 131 - 25;
return Math.min(fH, h);
};
const loadImage = cIndex => {
if (imageSizes.current[cIndex] || cIndex >= images.length) {
return;
}
Image.getSize(
images[cIndex].image?.uri,
(width, height) => {
imageSizes.current[cIndex] = {
width,
height: clampHeight(height),
};
continuous(cIndex + 1);
},
() => {
try {
const data = Image.resolveAssetSource(images[cIndex].image);
imageSizes.current[cIndex] = {
width: data.width,
height: clampHeight(data.height),
};
continuous(cIndex + 1);
} catch (newError) {
imageSizes.current[cIndex] = {
width: 0,
height: 0,
};
continuous(cIndex + 1);
}
},
);
};
const scrollFooter = (animated = true) => {
"worklet";
const fIndex = clamp(aIndex.value - 1, 0, imgLen.value - 3);
const nOffset = fIndex * (fImgSize + 20);
if (
nOffset <= scrollOffset.value ||
nOffset >= scrollOffset.value + dimenAni.x.value - 70
) {
scrollTo(fScroll, nOffset, 0, animated);
}
};
const onScroll = useAnimatedScrollHandler({
onScroll: ({ contentOffset: { x } }) => {
scrollOffset.value = x;
},
onMomentumEnd: ({ contentOffset: { x } }) => {
const fW = dimenAni.x.value;
const fI = (x / fW).toFixed();
const cIndex = Math.min(Math.max(fI, 0), imgLen.value);
mIndex.value = aIndex.value;
if (cIndex < 1) {
aIndex.value = imgLen.value - 2;
scrollTo(aref, aIndex.value * fW, 0, false);
} else if (cIndex >= imgLen.value - 1) {
aIndex.value = 1;
scrollTo(aref, fW, 0, false);
} else {
aIndex.value = cIndex;
}
scrollFooter();
if (mIndex.value != aIndex.value) {
animatedRefs.current[mIndex.value].vec.x.value = 0;
animatedRefs.current[mIndex.value].vec.y.value = 0;
animatedRefs.current[mIndex.value].scale.value = 1;
}
},
});
const onScrollFooter = useAnimatedScrollHandler({
onScroll: ({ contentOffset: { x } }) => {
scrollOffset.value = x;
},
});
return (
<SafeAreaView style={styles.container}>
<StatusBar backgroundColor="black" />
<View
style={[
styles.q,
{
paddingTop: 10,
},
]}>
<Icon onPress={onClose} name="close" size={24} color="white" />
</View>
<AnimatedFlatList
ref={aref}
data={images}
keyExtractor={(_, index) => `m_${index}`}
renderItem={renderMainItem}
horizontal
showsHorizontalScrollIndicator={false}
pagingEnabled={true}
onScroll={onScroll}
initialScrollIndex={aIndex.value}
getItemLayout={(data, index) => ({
length: dimensions.SCREEN_WIDTH,
offset: dimensions.SCREEN_WIDTH * index,
index,
})}
scrollEnabled={imgs?.length !== 1}
/>
<View style={[styles.f]}>
<View style={styles.e}>
<Text style={styles.tw}>{title}</Text>
<ReText text={indiText} style={styles.tw} />
</View>
<AnimatedFlatList
ref={fScroll}
data={imgs}
keyExtractor={(_, index) => `${index}`}
renderItem={renderItem}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingRight: 20 }}
onScroll={onScrollFooter}
/>
</View>
{render ? (
<View style={styles.y}>
<ActivityIndicator color="white" />
</View>
) : null}
</SafeAreaView>
);
};
export default ImageViewer;
const fImgSize = (dimensions.SCREEN_WIDTH - 140) / 4;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "black",
},
q: {
paddingLeft: 15,
paddingBottom: 15,
},
qq: {
flex: 1,
overflow: "hidden",
backgroundColor: "transparent",
},
tw: {
color: "white",
letterSpacing: 0.5,
fontWeight: "600",
},
f: {
// position: 'absolute',
},
e: {
flexDirection: "row",
justifyContent: "space-between",
paddingHorizontal: 20,
marginVertical: 15,
},
fItem: {
width: fImgSize,
height: fImgSize,
borderRadius: 12,
},
mItem: {},
r: {
marginLeft: 20,
borderWidth: 1,
borderRadius: 14,
},
t: {
flex: 1,
justifyContent: "center",
alignItems: "center",
width: dimensions.SCREEN_WIDTH,
},
y: {
justifyContent: "center",
alignItems: "center",
...StyleSheet.absoluteFillObject,
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment