Created
July 14, 2021 07:34
-
-
Save hungtrn75/fff763deee0d22ee13cc6a3eccdf5509 to your computer and use it in GitHub Desktop.
ImageViewer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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