react-native-gesture-image-viewer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +33 -0
- package/lib/module/GestureImageViewer.js +121 -0
- package/lib/module/GestureImageViewer.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useGestureImageViewer.js +145 -0
- package/lib/module/useGestureImageViewer.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/GestureImageViewer.d.ts +3 -0
- package/lib/typescript/src/GestureImageViewer.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +29 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/useGestureImageViewer.d.ts +24 -0
- package/lib/typescript/src/useGestureImageViewer.d.ts.map +1 -0
- package/package.json +127 -0
- package/src/GestureImageViewer.tsx +119 -0
- package/src/index.tsx +2 -0
- package/src/types.ts +30 -0
- package/src/useGestureImageViewer.ts +178 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 saseungmin
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# react-native-gesture-image-viewer
|
|
2
|
+
|
|
3
|
+
react-native-gesture-image-viewer
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install react-native-gesture-image-viewer
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import { multiply } from 'react-native-gesture-image-viewer';
|
|
16
|
+
|
|
17
|
+
// ...
|
|
18
|
+
|
|
19
|
+
const result = await multiply(3, 7);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## Contributing
|
|
24
|
+
|
|
25
|
+
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
|
|
29
|
+
MIT
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
|
+
import { FlatList, Platform, StyleSheet, useWindowDimensions, View } from 'react-native';
|
|
5
|
+
import { GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
6
|
+
import Animated from 'react-native-reanimated';
|
|
7
|
+
import { useGestureImageViewer } from "./useGestureImageViewer.js";
|
|
8
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
9
|
+
const WebPagingFix = () => {
|
|
10
|
+
if (Platform.OS !== 'web') {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return /*#__PURE__*/_jsx("style", {
|
|
14
|
+
children: `[data-paging-enabled-fix] > div > div > div {height: 100%;}`
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
export function GestureImageViewer({
|
|
18
|
+
data,
|
|
19
|
+
renderImage,
|
|
20
|
+
renderContainer,
|
|
21
|
+
ListComponent = FlatList,
|
|
22
|
+
width: customWidth,
|
|
23
|
+
listProps,
|
|
24
|
+
backdropStyle: backdropStyleProps,
|
|
25
|
+
containerStyle,
|
|
26
|
+
initialIndex = 0,
|
|
27
|
+
...props
|
|
28
|
+
}) {
|
|
29
|
+
const {
|
|
30
|
+
width: screenWidth
|
|
31
|
+
} = useWindowDimensions();
|
|
32
|
+
const width = customWidth || screenWidth;
|
|
33
|
+
const {
|
|
34
|
+
flatListRef,
|
|
35
|
+
panGesture,
|
|
36
|
+
onMomentumScrollEnd,
|
|
37
|
+
animatedStyle,
|
|
38
|
+
backdropStyle
|
|
39
|
+
} = useGestureImageViewer({
|
|
40
|
+
data,
|
|
41
|
+
width,
|
|
42
|
+
initialIndex,
|
|
43
|
+
...props
|
|
44
|
+
});
|
|
45
|
+
const renderItem = useCallback(({
|
|
46
|
+
item,
|
|
47
|
+
index
|
|
48
|
+
}) => {
|
|
49
|
+
return /*#__PURE__*/_jsx(Animated.View, {
|
|
50
|
+
style: {
|
|
51
|
+
width,
|
|
52
|
+
height: '100%',
|
|
53
|
+
justifyContent: 'center',
|
|
54
|
+
alignItems: 'center'
|
|
55
|
+
},
|
|
56
|
+
children: renderImage(item, index)
|
|
57
|
+
});
|
|
58
|
+
}, [width, renderImage]);
|
|
59
|
+
const getItemLayout = useCallback((_, index) => ({
|
|
60
|
+
length: width,
|
|
61
|
+
offset: width * index,
|
|
62
|
+
index
|
|
63
|
+
}), [width]);
|
|
64
|
+
const keyExtractor = useCallback((item, index) => `image-${index}-${item}`, []);
|
|
65
|
+
const listComponent = /*#__PURE__*/_jsx(GestureHandlerRootView, {
|
|
66
|
+
children: /*#__PURE__*/_jsx(GestureDetector, {
|
|
67
|
+
gesture: panGesture,
|
|
68
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
69
|
+
style: [styles.container, containerStyle],
|
|
70
|
+
children: [/*#__PURE__*/_jsx(Animated.View, {
|
|
71
|
+
style: [styles.background, backdropStyleProps, backdropStyle]
|
|
72
|
+
}), /*#__PURE__*/_jsx(Animated.View, {
|
|
73
|
+
style: [styles.content, animatedStyle],
|
|
74
|
+
children: /*#__PURE__*/_jsx(ListComponent, {
|
|
75
|
+
ref: flatListRef,
|
|
76
|
+
data: data,
|
|
77
|
+
renderItem: renderItem,
|
|
78
|
+
keyExtractor: keyExtractor,
|
|
79
|
+
horizontal: true,
|
|
80
|
+
pagingEnabled: true,
|
|
81
|
+
showsHorizontalScrollIndicator: false,
|
|
82
|
+
onMomentumScrollEnd: onMomentumScrollEnd,
|
|
83
|
+
getItemLayout: getItemLayout,
|
|
84
|
+
initialScrollIndex: initialIndex,
|
|
85
|
+
windowSize: 3,
|
|
86
|
+
maxToRenderPerBatch: 3,
|
|
87
|
+
removeClippedSubviews: true
|
|
88
|
+
// NOTE - https://github.com/necolas/react-native-web/issues/1299
|
|
89
|
+
,
|
|
90
|
+
...(Platform.OS === 'web' && {
|
|
91
|
+
dataSet: {
|
|
92
|
+
'paging-enabled-fix': true
|
|
93
|
+
}
|
|
94
|
+
}),
|
|
95
|
+
...listProps
|
|
96
|
+
})
|
|
97
|
+
}), /*#__PURE__*/_jsx(WebPagingFix, {})]
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
});
|
|
101
|
+
return renderContainer ? renderContainer(listComponent) : listComponent;
|
|
102
|
+
}
|
|
103
|
+
const styles = StyleSheet.create({
|
|
104
|
+
container: {
|
|
105
|
+
flex: 1
|
|
106
|
+
},
|
|
107
|
+
content: {
|
|
108
|
+
flex: 1,
|
|
109
|
+
width: '100%',
|
|
110
|
+
height: '100%'
|
|
111
|
+
},
|
|
112
|
+
background: {
|
|
113
|
+
position: 'absolute',
|
|
114
|
+
top: 0,
|
|
115
|
+
left: 0,
|
|
116
|
+
right: 0,
|
|
117
|
+
bottom: 0,
|
|
118
|
+
backgroundColor: 'black'
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
//# sourceMappingURL=GestureImageViewer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["useCallback","FlatList","Platform","StyleSheet","useWindowDimensions","View","GestureDetector","GestureHandlerRootView","Animated","useGestureImageViewer","jsx","_jsx","jsxs","_jsxs","WebPagingFix","OS","children","GestureImageViewer","data","renderImage","renderContainer","ListComponent","width","customWidth","listProps","backdropStyle","backdropStyleProps","containerStyle","initialIndex","props","screenWidth","flatListRef","panGesture","onMomentumScrollEnd","animatedStyle","renderItem","item","index","style","height","justifyContent","alignItems","getItemLayout","_","length","offset","keyExtractor","listComponent","gesture","styles","container","background","content","ref","horizontal","pagingEnabled","showsHorizontalScrollIndicator","initialScrollIndex","windowSize","maxToRenderPerBatch","removeClippedSubviews","dataSet","create","flex","position","top","left","right","bottom","backgroundColor"],"sourceRoot":"../../src","sources":["GestureImageViewer.tsx"],"mappings":";;AAAA,SAASA,WAAW,QAAQ,OAAO;AACnC,SAASC,QAAQ,EAAEC,QAAQ,EAAEC,UAAU,EAAEC,mBAAmB,EAAEC,IAAI,QAAQ,cAAc;AACxF,SAASC,eAAe,EAAEC,sBAAsB,QAAQ,8BAA8B;AACtF,OAAOC,QAAQ,MAAM,yBAAyB;AAE9C,SAASC,qBAAqB,QAAQ,4BAAyB;AAAC,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AAEhE,MAAMC,YAAY,GAAGA,CAAA,KAAM;EACzB,IAAIZ,QAAQ,CAACa,EAAE,KAAK,KAAK,EAAE;IACzB,OAAO,IAAI;EACb;EAEA,oBAAOJ,IAAA;IAAAK,QAAA,EAAQ;EAA6D,CAAQ,CAAC;AACvF,CAAC;AAED,OAAO,SAASC,kBAAkBA,CAAU;EAC1CC,IAAI;EACJC,WAAW;EACXC,eAAe;EACfC,aAAa,GAAGpB,QAAQ;EACxBqB,KAAK,EAAEC,WAAW;EAClBC,SAAS;EACTC,aAAa,EAAEC,kBAAkB;EACjCC,cAAc;EACdC,YAAY,GAAG,CAAC;EAChB,GAAGC;AACuB,CAAC,EAAE;EAC7B,MAAM;IAAEP,KAAK,EAAEQ;EAAY,CAAC,GAAG1B,mBAAmB,CAAC,CAAC;EAEpD,MAAMkB,KAAK,GAAGC,WAAW,IAAIO,WAAW;EAExC,MAAM;IAAEC,WAAW;IAAEC,UAAU;IAAEC,mBAAmB;IAAEC,aAAa;IAAET;EAAc,CAAC,GAAGhB,qBAAqB,CAAC;IAC3GS,IAAI;IACJI,KAAK;IACLM,YAAY;IACZ,GAAGC;EACL,CAAC,CAAC;EAEF,MAAMM,UAAU,GAAGnC,WAAW,CAC5B,CAAC;IAAEoC,IAAI;IAAEC;EAAkC,CAAC,KAAK;IAC/C,oBACE1B,IAAA,CAACH,QAAQ,CAACH,IAAI;MACZiC,KAAK,EAAE;QACLhB,KAAK;QACLiB,MAAM,EAAE,MAAM;QACdC,cAAc,EAAE,QAAQ;QACxBC,UAAU,EAAE;MACd,CAAE;MAAAzB,QAAA,EAEDG,WAAW,CAACiB,IAAI,EAAEC,KAAK;IAAC,CACZ,CAAC;EAEpB,CAAC,EACD,CAACf,KAAK,EAAEH,WAAW,CACrB,CAAC;EAED,MAAMuB,aAAa,GAAG1C,WAAW,CAC/B,CAAC2C,CAAM,EAAEN,KAAa,MAAM;IAC1BO,MAAM,EAAEtB,KAAK;IACbuB,MAAM,EAAEvB,KAAK,GAAGe,KAAK;IACrBA;EACF,CAAC,CAAC,EACF,CAACf,KAAK,CACR,CAAC;EAED,MAAMwB,YAAY,GAAG9C,WAAW,CAAC,CAACoC,IAAO,EAAEC,KAAa,KAAK,SAASA,KAAK,IAAID,IAAI,EAAE,EAAE,EAAE,CAAC;EAE1F,MAAMW,aAAa,gBACjBpC,IAAA,CAACJ,sBAAsB;IAAAS,QAAA,eACrBL,IAAA,CAACL,eAAe;MAAC0C,OAAO,EAAEhB,UAAW;MAAAhB,QAAA,eACnCH,KAAA,CAACR,IAAI;QAACiC,KAAK,EAAE,CAACW,MAAM,CAACC,SAAS,EAAEvB,cAAc,CAAE;QAAAX,QAAA,gBAC9CL,IAAA,CAACH,QAAQ,CAACH,IAAI;UAACiC,KAAK,EAAE,CAACW,MAAM,CAACE,UAAU,EAAEzB,kBAAkB,EAAED,aAAa;QAAE,CAAE,CAAC,eAChFd,IAAA,CAACH,QAAQ,CAACH,IAAI;UAACiC,KAAK,EAAE,CAACW,MAAM,CAACG,OAAO,EAAElB,aAAa,CAAE;UAAAlB,QAAA,eACpDL,IAAA,CAACU,aAAa;YACZgC,GAAG,EAAEtB,WAAY;YACjBb,IAAI,EAAEA,IAAK;YACXiB,UAAU,EAAEA,UAAW;YACvBW,YAAY,EAAEA,YAAa;YAC3BQ,UAAU;YACVC,aAAa;YACbC,8BAA8B,EAAE,KAAM;YACtCvB,mBAAmB,EAAEA,mBAAoB;YACzCS,aAAa,EAAEA,aAAc;YAC7Be,kBAAkB,EAAE7B,YAAa;YACjC8B,UAAU,EAAE,CAAE;YACdC,mBAAmB,EAAE,CAAE;YACvBC,qBAAqB,EAAE;YACvB;YAAA;YAAA,IACK1D,QAAQ,CAACa,EAAE,KAAK,KAAK,IAAI;cAAE8C,OAAO,EAAE;gBAAE,oBAAoB,EAAE;cAAK;YAAE,CAAC;YAAA,GACrErC;UAAS,CACd;QAAC,CACW,CAAC,eAChBb,IAAA,CAACG,YAAY,IAAE,CAAC;MAAA,CACZ;IAAC,CACQ;EAAC,CACI,CACzB;EAED,OAAOM,eAAe,GAAGA,eAAe,CAAC2B,aAAa,CAAC,GAAGA,aAAa;AACzE;AAEA,MAAME,MAAM,GAAG9C,UAAU,CAAC2D,MAAM,CAAC;EAC/BZ,SAAS,EAAE;IACTa,IAAI,EAAE;EACR,CAAC;EACDX,OAAO,EAAE;IACPW,IAAI,EAAE,CAAC;IACPzC,KAAK,EAAE,MAAM;IACbiB,MAAM,EAAE;EACV,CAAC;EACDY,UAAU,EAAE;IACVa,QAAQ,EAAE,UAAU;IACpBC,GAAG,EAAE,CAAC;IACNC,IAAI,EAAE,CAAC;IACPC,KAAK,EAAE,CAAC;IACRC,MAAM,EAAE,CAAC;IACTC,eAAe,EAAE;EACnB;AACF,CAAC,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["GestureImageViewer"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,kBAAkB,QAAQ,yBAAsB","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../src","sources":["types.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { InteractionManager, useWindowDimensions } from 'react-native';
|
|
5
|
+
import { Gesture } from 'react-native-gesture-handler';
|
|
6
|
+
import { interpolate, runOnJS, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
|
|
7
|
+
export const useGestureImageViewer = ({
|
|
8
|
+
data,
|
|
9
|
+
initialIndex = 0,
|
|
10
|
+
onIndexChange,
|
|
11
|
+
onDismiss,
|
|
12
|
+
width: customWidth,
|
|
13
|
+
dismissThreshold = 80,
|
|
14
|
+
resistance = 2,
|
|
15
|
+
// swipeThreshold = 0.5,
|
|
16
|
+
// velocityThreshold = 200,
|
|
17
|
+
animateBackdrop = true,
|
|
18
|
+
enableDismissGesture = true,
|
|
19
|
+
enableSwipeGesture = true
|
|
20
|
+
}) => {
|
|
21
|
+
const {
|
|
22
|
+
width: screenWidth
|
|
23
|
+
} = useWindowDimensions();
|
|
24
|
+
const width = customWidth || screenWidth;
|
|
25
|
+
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
|
26
|
+
const translateY = useSharedValue(0);
|
|
27
|
+
const backdropOpacity = useSharedValue(1);
|
|
28
|
+
const flatListRef = useRef(null);
|
|
29
|
+
const dataLength = data?.length || 0;
|
|
30
|
+
|
|
31
|
+
// 초기 위치 설정
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
setCurrentIndex(initialIndex);
|
|
34
|
+
translateY.value = 0;
|
|
35
|
+
backdropOpacity.value = 1;
|
|
36
|
+
if (initialIndex <= 0 || !flatListRef.current) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// FlatList 초기 위치 설정
|
|
41
|
+
const runAfterInteractions = InteractionManager.runAfterInteractions(() => {
|
|
42
|
+
flatListRef.current?.scrollToIndex({
|
|
43
|
+
index: initialIndex,
|
|
44
|
+
animated: false
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
return () => {
|
|
48
|
+
runAfterInteractions?.cancel();
|
|
49
|
+
};
|
|
50
|
+
}, [initialIndex, translateY, backdropOpacity]);
|
|
51
|
+
|
|
52
|
+
// 인덱스 변경 알림
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
onIndexChange?.(currentIndex);
|
|
55
|
+
}, [currentIndex, onIndexChange]);
|
|
56
|
+
|
|
57
|
+
// FlatList 스크롤 핸들러
|
|
58
|
+
const onMomentumScrollEnd = useCallback(event => {
|
|
59
|
+
if (!enableSwipeGesture) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const contentOffset = event.nativeEvent.contentOffset;
|
|
63
|
+
const newIndex = Math.round(contentOffset.x / width);
|
|
64
|
+
if (newIndex !== currentIndex && newIndex >= 0 && newIndex < dataLength) {
|
|
65
|
+
setCurrentIndex(newIndex);
|
|
66
|
+
}
|
|
67
|
+
}, [currentIndex, dataLength, width, enableSwipeGesture]);
|
|
68
|
+
const goToIndex = useCallback(index => {
|
|
69
|
+
if (index < 0 || index >= dataLength || !enableSwipeGesture) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
setCurrentIndex(index);
|
|
73
|
+
flatListRef.current?.scrollToIndex({
|
|
74
|
+
index,
|
|
75
|
+
animated: true
|
|
76
|
+
});
|
|
77
|
+
}, [dataLength, enableSwipeGesture]);
|
|
78
|
+
const goToPrevious = useCallback(() => {
|
|
79
|
+
if (currentIndex > 0) {
|
|
80
|
+
goToIndex(currentIndex - 1);
|
|
81
|
+
}
|
|
82
|
+
}, [currentIndex, goToIndex]);
|
|
83
|
+
const goToNext = useCallback(() => {
|
|
84
|
+
if (currentIndex < dataLength - 1) {
|
|
85
|
+
goToIndex(currentIndex + 1);
|
|
86
|
+
}
|
|
87
|
+
}, [currentIndex, dataLength, goToIndex]);
|
|
88
|
+
const panGesture = useMemo(() => {
|
|
89
|
+
return Gesture.Pan().minDistance(10).activeOffsetY([-10, 10]).failOffsetX([-10, 10]).onUpdate(event => {
|
|
90
|
+
'worklet';
|
|
91
|
+
|
|
92
|
+
translateY.value = event.translationY / resistance;
|
|
93
|
+
}).onEnd(event => {
|
|
94
|
+
'worklet';
|
|
95
|
+
|
|
96
|
+
if (event.translationY > dismissThreshold && enableDismissGesture && onDismiss) {
|
|
97
|
+
runOnJS(onDismiss)();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
translateY.value = withSpring(0, {
|
|
101
|
+
damping: 15,
|
|
102
|
+
stiffness: 150
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}, [translateY, dismissThreshold, enableDismissGesture, onDismiss, resistance]);
|
|
106
|
+
|
|
107
|
+
// 애니메이션 스타일
|
|
108
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
109
|
+
return {
|
|
110
|
+
transform: [{
|
|
111
|
+
translateY: translateY.value
|
|
112
|
+
}]
|
|
113
|
+
};
|
|
114
|
+
}, [translateY]);
|
|
115
|
+
const backdropStyle = useAnimatedStyle(() => {
|
|
116
|
+
if (!animateBackdrop) {
|
|
117
|
+
return {
|
|
118
|
+
opacity: 1
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const opacity = interpolate(translateY.value, [0, 200], [1, 0], 'clamp');
|
|
122
|
+
return {
|
|
123
|
+
opacity
|
|
124
|
+
};
|
|
125
|
+
}, [animateBackdrop, translateY]);
|
|
126
|
+
return {
|
|
127
|
+
// State
|
|
128
|
+
currentIndex,
|
|
129
|
+
dataLength,
|
|
130
|
+
translateY,
|
|
131
|
+
flatListRef,
|
|
132
|
+
// Actions
|
|
133
|
+
goToPrevious,
|
|
134
|
+
goToNext,
|
|
135
|
+
goToIndex,
|
|
136
|
+
// Gesture
|
|
137
|
+
panGesture,
|
|
138
|
+
// FlatList handlers
|
|
139
|
+
onMomentumScrollEnd,
|
|
140
|
+
// Animated styles
|
|
141
|
+
animatedStyle,
|
|
142
|
+
backdropStyle
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
//# sourceMappingURL=useGestureImageViewer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["useCallback","useEffect","useMemo","useRef","useState","InteractionManager","useWindowDimensions","Gesture","interpolate","runOnJS","useAnimatedStyle","useSharedValue","withSpring","useGestureImageViewer","data","initialIndex","onIndexChange","onDismiss","width","customWidth","dismissThreshold","resistance","animateBackdrop","enableDismissGesture","enableSwipeGesture","screenWidth","currentIndex","setCurrentIndex","translateY","backdropOpacity","flatListRef","dataLength","length","value","current","runAfterInteractions","scrollToIndex","index","animated","cancel","onMomentumScrollEnd","event","contentOffset","nativeEvent","newIndex","Math","round","x","goToIndex","goToPrevious","goToNext","panGesture","Pan","minDistance","activeOffsetY","failOffsetX","onUpdate","translationY","onEnd","damping","stiffness","animatedStyle","transform","backdropStyle","opacity"],"sourceRoot":"../../src","sources":["useGestureImageViewer.ts"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { GestureImageViewerProps } from './types';
|
|
2
|
+
export declare function GestureImageViewer<T = any>({ data, renderImage, renderContainer, ListComponent, width: customWidth, listProps, backdropStyle: backdropStyleProps, containerStyle, initialIndex, ...props }: GestureImageViewerProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
//# sourceMappingURL=GestureImageViewer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GestureImageViewer.d.ts","sourceRoot":"","sources":["../../../src/GestureImageViewer.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAWvD,wBAAgB,kBAAkB,CAAC,CAAC,GAAG,GAAG,EAAE,EAC1C,IAAI,EACJ,WAAW,EACX,eAAe,EACf,aAAwB,EACxB,KAAK,EAAE,WAAW,EAClB,SAAS,EACT,aAAa,EAAE,kBAAkB,EACjC,cAAc,EACd,YAAgB,EAChB,GAAG,KAAK,EACT,EAAE,uBAAuB,CAAC,CAAC,CAAC,2CAyE5B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,YAAY,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import type { SharedValue } from 'react-native-reanimated';
|
|
3
|
+
export interface GestureImageViewerProps<T = any> {
|
|
4
|
+
data: T[];
|
|
5
|
+
initialIndex?: number;
|
|
6
|
+
onIndexChange?: (index: number) => void;
|
|
7
|
+
onDismiss?: () => void;
|
|
8
|
+
renderImage: (item: T, index: number) => React.ReactElement;
|
|
9
|
+
renderContainer?: (children: React.ReactElement) => React.ReactElement;
|
|
10
|
+
ListComponent?: any;
|
|
11
|
+
width?: number;
|
|
12
|
+
dismissThreshold?: number;
|
|
13
|
+
swipeThreshold?: number;
|
|
14
|
+
velocityThreshold?: number;
|
|
15
|
+
enableDismissGesture?: boolean;
|
|
16
|
+
enableSwipeGesture?: boolean;
|
|
17
|
+
resistance?: number;
|
|
18
|
+
listProps?: any;
|
|
19
|
+
backdropStyle?: StyleProp<ViewStyle>;
|
|
20
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
21
|
+
animateBackdrop?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface GestureState {
|
|
24
|
+
currentIndex: number;
|
|
25
|
+
translateY: SharedValue<number>;
|
|
26
|
+
isGestureActive: boolean;
|
|
27
|
+
lockDirection: 'horizontal' | 'vertical' | null;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,MAAM,WAAW,uBAAuB,CAAC,CAAC,GAAG,GAAG;IAC9C,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC,YAAY,CAAC;IAC5D,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,YAAY,KAAK,KAAK,CAAC,YAAY,CAAC;IACvE,aAAa,CAAC,EAAE,GAAG,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,GAAG,CAAC;IAChB,aAAa,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACrC,cAAc,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACtC,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAChC,eAAe,EAAE,OAAO,CAAC;IACzB,aAAa,EAAE,YAAY,GAAG,UAAU,GAAG,IAAI,CAAC;CACjD"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type NativeScrollEvent, type NativeSyntheticEvent } from 'react-native';
|
|
2
|
+
import type { GestureImageViewerProps } from './types';
|
|
3
|
+
type UseGestureImageViewerProps<T = any> = Omit<GestureImageViewerProps<T>, 'renderImage' | 'renderContainer' | 'ListComponent' | 'listProps' | 'containerStyle' | 'backdropStyle'>;
|
|
4
|
+
export declare const useGestureImageViewer: <T = any>({ data, initialIndex, onIndexChange, onDismiss, width: customWidth, dismissThreshold, resistance, animateBackdrop, enableDismissGesture, enableSwipeGesture, }: UseGestureImageViewerProps<T>) => {
|
|
5
|
+
currentIndex: number;
|
|
6
|
+
dataLength: number;
|
|
7
|
+
translateY: import("react-native-reanimated").SharedValue<number>;
|
|
8
|
+
flatListRef: import("react").RefObject<any>;
|
|
9
|
+
goToPrevious: () => void;
|
|
10
|
+
goToNext: () => void;
|
|
11
|
+
goToIndex: (index: number) => void;
|
|
12
|
+
panGesture: import("react-native-gesture-handler/lib/typescript/handlers/gestures/panGesture").PanGesture;
|
|
13
|
+
onMomentumScrollEnd: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
14
|
+
animatedStyle: {
|
|
15
|
+
transform: {
|
|
16
|
+
translateY: number;
|
|
17
|
+
}[];
|
|
18
|
+
};
|
|
19
|
+
backdropStyle: {
|
|
20
|
+
opacity: number;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=useGestureImageViewer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGestureImageViewer.d.ts","sourceRoot":"","sources":["../../../src/useGestureImageViewer.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAE1B,MAAM,cAAc,CAAC;AAGtB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAEvD,KAAK,0BAA0B,CAAC,CAAC,GAAG,GAAG,IAAI,IAAI,CAC7C,uBAAuB,CAAC,CAAC,CAAC,EAC1B,aAAa,GAAG,iBAAiB,GAAG,eAAe,GAAG,WAAW,GAAG,gBAAgB,GAAG,eAAe,CACvG,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAI,CAAC,GAAG,GAAG,EAAE,gKAa5C,0BAA0B,CAAC,CAAC,CAAC;;;;;;;uBA2DpB,MAAM;;iCAhBN,oBAAoB,CAAC,iBAAiB,CAAC;;;;;;;;;CAyGlD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-gesture-image-viewer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "react-native-gesture-image-viewer",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"source": "./src/index.tsx",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"lib",
|
|
18
|
+
"android",
|
|
19
|
+
"ios",
|
|
20
|
+
"cpp",
|
|
21
|
+
"*.podspec",
|
|
22
|
+
"react-native.config.js",
|
|
23
|
+
"!ios/build",
|
|
24
|
+
"!android/build",
|
|
25
|
+
"!android/gradle",
|
|
26
|
+
"!android/gradlew",
|
|
27
|
+
"!android/gradlew.bat",
|
|
28
|
+
"!android/local.properties",
|
|
29
|
+
"!**/__tests__",
|
|
30
|
+
"!**/__fixtures__",
|
|
31
|
+
"!**/__mocks__",
|
|
32
|
+
"!**/.*"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"example": "yarn workspace react-native-gesture-image-viewer-example run",
|
|
36
|
+
"test": "jest",
|
|
37
|
+
"typecheck": "tsc",
|
|
38
|
+
"lint": "biome format --write",
|
|
39
|
+
"clean": "del-cli lib",
|
|
40
|
+
"build": "bob build",
|
|
41
|
+
"changeset": "changeset"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"react-native",
|
|
45
|
+
"ios",
|
|
46
|
+
"android"
|
|
47
|
+
],
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/saseungmin/react-native-gesture-image-viewer.git"
|
|
51
|
+
},
|
|
52
|
+
"author": "saseungmin <dbd02169@naver.com> (https://github.com/saseungmin)",
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/saseungmin/react-native-gesture-image-viewer/issues"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/saseungmin/react-native-gesture-image-viewer#readme",
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"registry": "https://registry.npmjs.org/"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@babel/core": "^7.27.7",
|
|
63
|
+
"@biomejs/biome": "^2.0.6",
|
|
64
|
+
"@changesets/cli": "^2.29.5",
|
|
65
|
+
"@commitlint/config-conventional": "^19.6.0",
|
|
66
|
+
"@evilmartians/lefthook": "^1.5.0",
|
|
67
|
+
"@react-native/babel-preset": "0.78.2",
|
|
68
|
+
"@types/babel__core": "^7",
|
|
69
|
+
"@types/jest": "^29.5.5",
|
|
70
|
+
"@types/react": "^19.0.12",
|
|
71
|
+
"commitlint": "^19.6.1",
|
|
72
|
+
"del-cli": "^5.1.0",
|
|
73
|
+
"jest": "^29.7.0",
|
|
74
|
+
"react": "19.0.0",
|
|
75
|
+
"react-native": "0.79.4",
|
|
76
|
+
"react-native-builder-bob": "^0.40.12",
|
|
77
|
+
"react-native-gesture-handler": "^2.24.0",
|
|
78
|
+
"react-native-reanimated": "^3.18.0",
|
|
79
|
+
"typescript": "^5.8.3"
|
|
80
|
+
},
|
|
81
|
+
"peerDependencies": {
|
|
82
|
+
"react": ">=18.0.0",
|
|
83
|
+
"react-native": ">=0.75.0",
|
|
84
|
+
"react-native-gesture-handler": ">=2.24.0",
|
|
85
|
+
"react-native-reanimated": ">=3.0.0"
|
|
86
|
+
},
|
|
87
|
+
"workspaces": [
|
|
88
|
+
".",
|
|
89
|
+
"example"
|
|
90
|
+
],
|
|
91
|
+
"packageManager": "yarn@4.9.2",
|
|
92
|
+
"jest": {
|
|
93
|
+
"preset": "react-native",
|
|
94
|
+
"modulePathIgnorePatterns": [
|
|
95
|
+
"<rootDir>/example/node_modules",
|
|
96
|
+
"<rootDir>/lib/"
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
"commitlint": {
|
|
100
|
+
"extends": [
|
|
101
|
+
"@commitlint/config-conventional"
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
"react-native-builder-bob": {
|
|
105
|
+
"source": "src",
|
|
106
|
+
"output": "lib",
|
|
107
|
+
"targets": [
|
|
108
|
+
[
|
|
109
|
+
"module",
|
|
110
|
+
{
|
|
111
|
+
"esm": true
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
[
|
|
115
|
+
"typescript",
|
|
116
|
+
{
|
|
117
|
+
"project": "tsconfig.build.json"
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
"create-react-native-library": {
|
|
123
|
+
"languages": "js",
|
|
124
|
+
"type": "library",
|
|
125
|
+
"version": "0.51.1"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { FlatList, Platform, StyleSheet, useWindowDimensions, View } from 'react-native';
|
|
3
|
+
import { GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
4
|
+
import Animated from 'react-native-reanimated';
|
|
5
|
+
import type { GestureImageViewerProps } from './types';
|
|
6
|
+
import { useGestureImageViewer } from './useGestureImageViewer';
|
|
7
|
+
|
|
8
|
+
const WebPagingFix = () => {
|
|
9
|
+
if (Platform.OS !== 'web') {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return <style>{`[data-paging-enabled-fix] > div > div > div {height: 100%;}`}</style>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function GestureImageViewer<T = any>({
|
|
17
|
+
data,
|
|
18
|
+
renderImage,
|
|
19
|
+
renderContainer,
|
|
20
|
+
ListComponent = FlatList,
|
|
21
|
+
width: customWidth,
|
|
22
|
+
listProps,
|
|
23
|
+
backdropStyle: backdropStyleProps,
|
|
24
|
+
containerStyle,
|
|
25
|
+
initialIndex = 0,
|
|
26
|
+
...props
|
|
27
|
+
}: GestureImageViewerProps<T>) {
|
|
28
|
+
const { width: screenWidth } = useWindowDimensions();
|
|
29
|
+
|
|
30
|
+
const width = customWidth || screenWidth;
|
|
31
|
+
|
|
32
|
+
const { flatListRef, panGesture, onMomentumScrollEnd, animatedStyle, backdropStyle } = useGestureImageViewer({
|
|
33
|
+
data,
|
|
34
|
+
width,
|
|
35
|
+
initialIndex,
|
|
36
|
+
...props,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const renderItem = useCallback(
|
|
40
|
+
({ item, index }: { item: T; index: number }) => {
|
|
41
|
+
return (
|
|
42
|
+
<Animated.View
|
|
43
|
+
style={{
|
|
44
|
+
width,
|
|
45
|
+
height: '100%',
|
|
46
|
+
justifyContent: 'center',
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{renderImage(item, index)}
|
|
51
|
+
</Animated.View>
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
[width, renderImage],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const getItemLayout = useCallback(
|
|
58
|
+
(_: any, index: number) => ({
|
|
59
|
+
length: width,
|
|
60
|
+
offset: width * index,
|
|
61
|
+
index,
|
|
62
|
+
}),
|
|
63
|
+
[width],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const keyExtractor = useCallback((item: T, index: number) => `image-${index}-${item}`, []);
|
|
67
|
+
|
|
68
|
+
const listComponent = (
|
|
69
|
+
<GestureHandlerRootView>
|
|
70
|
+
<GestureDetector gesture={panGesture}>
|
|
71
|
+
<View style={[styles.container, containerStyle]}>
|
|
72
|
+
<Animated.View style={[styles.background, backdropStyleProps, backdropStyle]} />
|
|
73
|
+
<Animated.View style={[styles.content, animatedStyle]}>
|
|
74
|
+
<ListComponent
|
|
75
|
+
ref={flatListRef}
|
|
76
|
+
data={data}
|
|
77
|
+
renderItem={renderItem}
|
|
78
|
+
keyExtractor={keyExtractor}
|
|
79
|
+
horizontal
|
|
80
|
+
pagingEnabled
|
|
81
|
+
showsHorizontalScrollIndicator={false}
|
|
82
|
+
onMomentumScrollEnd={onMomentumScrollEnd}
|
|
83
|
+
getItemLayout={getItemLayout}
|
|
84
|
+
initialScrollIndex={initialIndex}
|
|
85
|
+
windowSize={3}
|
|
86
|
+
maxToRenderPerBatch={3}
|
|
87
|
+
removeClippedSubviews={true}
|
|
88
|
+
// NOTE - https://github.com/necolas/react-native-web/issues/1299
|
|
89
|
+
{...(Platform.OS === 'web' && { dataSet: { 'paging-enabled-fix': true } })}
|
|
90
|
+
{...listProps}
|
|
91
|
+
/>
|
|
92
|
+
</Animated.View>
|
|
93
|
+
<WebPagingFix />
|
|
94
|
+
</View>
|
|
95
|
+
</GestureDetector>
|
|
96
|
+
</GestureHandlerRootView>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return renderContainer ? renderContainer(listComponent) : listComponent;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const styles = StyleSheet.create({
|
|
103
|
+
container: {
|
|
104
|
+
flex: 1,
|
|
105
|
+
},
|
|
106
|
+
content: {
|
|
107
|
+
flex: 1,
|
|
108
|
+
width: '100%',
|
|
109
|
+
height: '100%',
|
|
110
|
+
},
|
|
111
|
+
background: {
|
|
112
|
+
position: 'absolute',
|
|
113
|
+
top: 0,
|
|
114
|
+
left: 0,
|
|
115
|
+
right: 0,
|
|
116
|
+
bottom: 0,
|
|
117
|
+
backgroundColor: 'black',
|
|
118
|
+
},
|
|
119
|
+
});
|
package/src/index.tsx
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import type { SharedValue } from 'react-native-reanimated';
|
|
3
|
+
|
|
4
|
+
export interface GestureImageViewerProps<T = any> {
|
|
5
|
+
data: T[];
|
|
6
|
+
initialIndex?: number;
|
|
7
|
+
onIndexChange?: (index: number) => void;
|
|
8
|
+
onDismiss?: () => void;
|
|
9
|
+
renderImage: (item: T, index: number) => React.ReactElement;
|
|
10
|
+
renderContainer?: (children: React.ReactElement) => React.ReactElement;
|
|
11
|
+
ListComponent?: any; // FlatList, FlashList 등
|
|
12
|
+
width?: number;
|
|
13
|
+
dismissThreshold?: number;
|
|
14
|
+
swipeThreshold?: number;
|
|
15
|
+
velocityThreshold?: number;
|
|
16
|
+
enableDismissGesture?: boolean;
|
|
17
|
+
enableSwipeGesture?: boolean;
|
|
18
|
+
resistance?: number;
|
|
19
|
+
listProps?: any; // FlatList, FlashList에 전달할 추가 props
|
|
20
|
+
backdropStyle?: StyleProp<ViewStyle>;
|
|
21
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
22
|
+
animateBackdrop?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface GestureState {
|
|
26
|
+
currentIndex: number;
|
|
27
|
+
translateY: SharedValue<number>;
|
|
28
|
+
isGestureActive: boolean;
|
|
29
|
+
lockDirection: 'horizontal' | 'vertical' | null;
|
|
30
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
InteractionManager,
|
|
4
|
+
type NativeScrollEvent,
|
|
5
|
+
type NativeSyntheticEvent,
|
|
6
|
+
useWindowDimensions,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
import { Gesture } from 'react-native-gesture-handler';
|
|
9
|
+
import { interpolate, runOnJS, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
|
|
10
|
+
import type { GestureImageViewerProps } from './types';
|
|
11
|
+
|
|
12
|
+
type UseGestureImageViewerProps<T = any> = Omit<
|
|
13
|
+
GestureImageViewerProps<T>,
|
|
14
|
+
'renderImage' | 'renderContainer' | 'ListComponent' | 'listProps' | 'containerStyle' | 'backdropStyle'
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
export const useGestureImageViewer = <T = any>({
|
|
18
|
+
data,
|
|
19
|
+
initialIndex = 0,
|
|
20
|
+
onIndexChange,
|
|
21
|
+
onDismiss,
|
|
22
|
+
width: customWidth,
|
|
23
|
+
dismissThreshold = 80,
|
|
24
|
+
resistance = 2,
|
|
25
|
+
// swipeThreshold = 0.5,
|
|
26
|
+
// velocityThreshold = 200,
|
|
27
|
+
animateBackdrop = true,
|
|
28
|
+
enableDismissGesture = true,
|
|
29
|
+
enableSwipeGesture = true,
|
|
30
|
+
}: UseGestureImageViewerProps<T>) => {
|
|
31
|
+
const { width: screenWidth } = useWindowDimensions();
|
|
32
|
+
const width = customWidth || screenWidth;
|
|
33
|
+
|
|
34
|
+
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
|
35
|
+
|
|
36
|
+
const translateY = useSharedValue(0);
|
|
37
|
+
const backdropOpacity = useSharedValue(1);
|
|
38
|
+
|
|
39
|
+
const flatListRef = useRef<any>(null);
|
|
40
|
+
|
|
41
|
+
const dataLength = data?.length || 0;
|
|
42
|
+
|
|
43
|
+
// 초기 위치 설정
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
setCurrentIndex(initialIndex);
|
|
46
|
+
translateY.value = 0;
|
|
47
|
+
backdropOpacity.value = 1;
|
|
48
|
+
|
|
49
|
+
if (initialIndex <= 0 || !flatListRef.current) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// FlatList 초기 위치 설정
|
|
54
|
+
const runAfterInteractions = InteractionManager.runAfterInteractions(() => {
|
|
55
|
+
flatListRef.current?.scrollToIndex({
|
|
56
|
+
index: initialIndex,
|
|
57
|
+
animated: false,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
runAfterInteractions?.cancel();
|
|
63
|
+
};
|
|
64
|
+
}, [initialIndex, translateY, backdropOpacity]);
|
|
65
|
+
|
|
66
|
+
// 인덱스 변경 알림
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
onIndexChange?.(currentIndex);
|
|
69
|
+
}, [currentIndex, onIndexChange]);
|
|
70
|
+
|
|
71
|
+
// FlatList 스크롤 핸들러
|
|
72
|
+
const onMomentumScrollEnd = useCallback(
|
|
73
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
74
|
+
if (!enableSwipeGesture) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const contentOffset = event.nativeEvent.contentOffset;
|
|
79
|
+
const newIndex = Math.round(contentOffset.x / width);
|
|
80
|
+
|
|
81
|
+
if (newIndex !== currentIndex && newIndex >= 0 && newIndex < dataLength) {
|
|
82
|
+
setCurrentIndex(newIndex);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
[currentIndex, dataLength, width, enableSwipeGesture],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const goToIndex = useCallback(
|
|
89
|
+
(index: number) => {
|
|
90
|
+
if (index < 0 || index >= dataLength || !enableSwipeGesture) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setCurrentIndex(index);
|
|
95
|
+
flatListRef.current?.scrollToIndex({
|
|
96
|
+
index,
|
|
97
|
+
animated: true,
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
[dataLength, enableSwipeGesture],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const goToPrevious = useCallback(() => {
|
|
104
|
+
if (currentIndex > 0) {
|
|
105
|
+
goToIndex(currentIndex - 1);
|
|
106
|
+
}
|
|
107
|
+
}, [currentIndex, goToIndex]);
|
|
108
|
+
|
|
109
|
+
const goToNext = useCallback(() => {
|
|
110
|
+
if (currentIndex < dataLength - 1) {
|
|
111
|
+
goToIndex(currentIndex + 1);
|
|
112
|
+
}
|
|
113
|
+
}, [currentIndex, dataLength, goToIndex]);
|
|
114
|
+
|
|
115
|
+
const panGesture = useMemo(() => {
|
|
116
|
+
return Gesture.Pan()
|
|
117
|
+
.minDistance(10)
|
|
118
|
+
.activeOffsetY([-10, 10])
|
|
119
|
+
.failOffsetX([-10, 10])
|
|
120
|
+
.onUpdate((event) => {
|
|
121
|
+
'worklet';
|
|
122
|
+
|
|
123
|
+
translateY.value = event.translationY / resistance;
|
|
124
|
+
})
|
|
125
|
+
.onEnd((event) => {
|
|
126
|
+
'worklet';
|
|
127
|
+
if (event.translationY > dismissThreshold && enableDismissGesture && onDismiss) {
|
|
128
|
+
runOnJS(onDismiss)();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
translateY.value = withSpring(0, {
|
|
133
|
+
damping: 15,
|
|
134
|
+
stiffness: 150,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}, [translateY, dismissThreshold, enableDismissGesture, onDismiss, resistance]);
|
|
138
|
+
|
|
139
|
+
// 애니메이션 스타일
|
|
140
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
141
|
+
return {
|
|
142
|
+
transform: [{ translateY: translateY.value }],
|
|
143
|
+
};
|
|
144
|
+
}, [translateY]);
|
|
145
|
+
|
|
146
|
+
const backdropStyle = useAnimatedStyle(() => {
|
|
147
|
+
if (!animateBackdrop) {
|
|
148
|
+
return { opacity: 1 };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const opacity = interpolate(translateY.value, [0, 200], [1, 0], 'clamp');
|
|
152
|
+
|
|
153
|
+
return { opacity };
|
|
154
|
+
}, [animateBackdrop, translateY]);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
// State
|
|
158
|
+
currentIndex,
|
|
159
|
+
dataLength,
|
|
160
|
+
translateY,
|
|
161
|
+
flatListRef,
|
|
162
|
+
|
|
163
|
+
// Actions
|
|
164
|
+
goToPrevious,
|
|
165
|
+
goToNext,
|
|
166
|
+
goToIndex,
|
|
167
|
+
|
|
168
|
+
// Gesture
|
|
169
|
+
panGesture,
|
|
170
|
+
|
|
171
|
+
// FlatList handlers
|
|
172
|
+
onMomentumScrollEnd,
|
|
173
|
+
|
|
174
|
+
// Animated styles
|
|
175
|
+
animatedStyle,
|
|
176
|
+
backdropStyle,
|
|
177
|
+
};
|
|
178
|
+
};
|