Open History to see the last commit
1diff --git a/dist/ImageViewing.js b/dist/ImageViewing.js
2index 3d7641a9a94864415d207c678b71f8c592c90aa1..2b9ef77c3eb58cd284152ad22b2e711a91d50e7d 100644
3--- a/dist/ImageViewing.js
4+++ b/dist/ImageViewing.js
5@@ -5,8 +5,8 @@
6 * LICENSE file in the root directory of this source tree.
7 *
8 */
9-import React, { useCallback, useRef, useEffect } from "react";
10-import { Animated, Dimensions, StyleSheet, View, VirtualizedList, Modal, } from "react-native";
11+import React, { useCallback, useRef, useEffect, useLayoutEffect } from "react";
12+import { Animated, Dimensions, StyleSheet, View, VirtualizedList, Modal, Platform, useWindowDimensions, } from "react-native";
13 import ImageItem from "./components/ImageItem/ImageItem";
14 import ImageDefaultHeader from "./components/ImageDefaultHeader";
15 import StatusBarManager from "./components/StatusBarManager";
16@@ -17,23 +17,34 @@ const DEFAULT_ANIMATION_TYPE = "fade";
17 const DEFAULT_BG_COLOR = "#000";
18 const DEFAULT_DELAY_LONG_PRESS = 800;
19 const SCREEN = Dimensions.get("screen");
20-const SCREEN_WIDTH = SCREEN.width;
21+const useViewingDimensions = Platform.OS === "android" ? useWindowDimensions : () => SCREEN;
22 function ImageViewing({ images, keyExtractor, imageIndex, visible, onRequestClose, onLongPress = () => { }, onImageIndexChange, animationType = DEFAULT_ANIMATION_TYPE, backgroundColor = DEFAULT_BG_COLOR, presentationStyle, swipeToCloseEnabled, doubleTapToZoomEnabled, delayLongPress = DEFAULT_DELAY_LONG_PRESS, HeaderComponent, FooterComponent, }) {
23+ const viewport = useViewingDimensions();
24+ const SCREEN_WIDTH = viewport.width;
25+ const viewportKey = Platform.OS === "android" ? `${viewport.width}:${viewport.height}` : undefined;
26 const imageList = useRef(null);
27 const [opacity, onRequestCloseEnhanced] = useRequestClose(onRequestClose);
28- const [currentImageIndex, onScroll] = useImageIndexChange(imageIndex, SCREEN);
29- const [headerTransform, footerTransform, toggleBarsVisible] = useAnimatedComponents();
30+ const [currentImageIndex, onScroll] = useImageIndexChange(imageIndex, viewport);
31+ const [headerTransform, footerTransform, toggleBarsVisible] = useAnimatedComponents(viewportKey);
32 useEffect(() => {
33 if (onImageIndexChange) {
34 onImageIndexChange(currentImageIndex);
35 }
36 }, [currentImageIndex]);
37- const onZoom = useCallback((isScaled) => {
38+ const activeZoom = useRef(null);
39+ const onZoom = useCallback(function handleZoom(isScaled) {
40+ if (Platform.OS === "android" && activeZoom.current !== handleZoom) {
41+ return;
42+ }
43 var _a, _b;
44 // @ts-ignore
45 (_b = (_a = imageList) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.setNativeProps({ scrollEnabled: !isScaled });
46 toggleBarsVisible(!isScaled);
47- }, [imageList]);
48+ }, [imageList, toggleBarsVisible]);
49+ useLayoutEffect(() => {
50+ activeZoom.current = onZoom;
51+ return () => { activeZoom.current = null; };
52+ }, [onZoom]);
53 if (!visible) {
54 return null;
55 }
56@@ -45,7 +56,8 @@ function ImageViewing({ images, keyExtractor, imageIndex, visible, onRequestClos
57 imageIndex: currentImageIndex,
58 })) : (<ImageDefaultHeader onRequestClose={onRequestCloseEnhanced}/>)}
59 </Animated.View>
60- <VirtualizedList ref={imageList} data={images} horizontal pagingEnabled windowSize={2} initialNumToRender={1} maxToRenderPerBatch={1} showsHorizontalScrollIndicator={false} showsVerticalScrollIndicator={false} initialScrollIndex={imageIndex} getItem={(_, index) => images[index]} getItemCount={() => images.length} getItemLayout={(_, index) => ({
61+ {/* Reset Android image gestures on resize while preserving custom header state. */}
62+ <VirtualizedList key={viewportKey} ref={imageList} onScroll={Platform.OS === "android" ? onScroll : undefined} scrollEventThrottle={Platform.OS === "android" ? 16 : undefined} data={images} horizontal pagingEnabled windowSize={2} initialNumToRender={1} maxToRenderPerBatch={1} showsHorizontalScrollIndicator={false} showsVerticalScrollIndicator={false} initialScrollIndex={currentImageIndex} getItem={(_, index) => images[index]} getItemCount={() => images.length} getItemLayout={(_, index) => ({
63 length: SCREEN_WIDTH,
64 offset: SCREEN_WIDTH * index,
65 index,
66diff --git a/dist/components/ImageItem/ImageItem.android.js b/dist/components/ImageItem/ImageItem.android.js
67index 5fcaa9f9a7d893817fee89b85186ee3936118e69..e166dd14349b4b95f96d648cb64f464ed564c8b8 100644
68--- a/dist/components/ImageItem/ImageItem.android.js
69+++ b/dist/components/ImageItem/ImageItem.android.js
70@@ -6,17 +6,17 @@
71 *
72 */
73 import React, { useCallback, useRef, useState } from "react";
74-import { Animated, ScrollView, Dimensions, StyleSheet, } from "react-native";
75+import { Animated, ScrollView, Dimensions, View, } from "react-native";
76 import useImageDimensions from "../../hooks/useImageDimensions";
77 import usePanResponder from "../../hooks/usePanResponder";
78 import { getImageStyles, getImageTransform } from "../../utils";
79 import { ImageLoading } from "./ImageLoading";
80 const SWIPE_CLOSE_OFFSET = 75;
81 const SWIPE_CLOSE_VELOCITY = 1.75;
82-const SCREEN = Dimensions.get("window");
83-const SCREEN_WIDTH = SCREEN.width;
84-const SCREEN_HEIGHT = SCREEN.height;
85 const ImageItem = ({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPress, swipeToCloseEnabled = true, doubleTapToZoomEnabled = true, }) => {
86+ const SCREEN = Dimensions.get("window");
87+ const SCREEN_WIDTH = SCREEN.width;
88+ const SCREEN_HEIGHT = SCREEN.height;
89 const imageContainer = useRef(null);
90 const imageDimensions = useImageDimensions(imageSrc);
91 const [translate, scale] = getImageTransform(imageDimensions, SCREEN);
92@@ -28,10 +28,10 @@ const ImageItem = ({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPre
93 onZoom(isZoomed);
94 if ((_a = imageContainer) === null || _a === void 0 ? void 0 : _a.current) {
95 imageContainer.current.setNativeProps({
96- scrollEnabled: !isZoomed,
97+ scrollEnabled: swipeToCloseEnabled && !isZoomed,
98 });
99 }
100- }, [imageContainer]);
101+ }, [imageContainer, onZoom, swipeToCloseEnabled]);
102 const onLongPressHandler = useCallback(() => {
103 onLongPress(imageSrc);
104 }, [imageSrc, onLongPress]);
105@@ -64,21 +64,15 @@ const ImageItem = ({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPre
106 const offsetY = (_c = (_b = (_a = nativeEvent) === null || _a === void 0 ? void 0 : _a.contentOffset) === null || _b === void 0 ? void 0 : _b.y, (_c !== null && _c !== void 0 ? _c : 0));
107 scrollValueY.setValue(offsetY);
108 };
109- return (<ScrollView ref={imageContainer} style={styles.listItem} pagingEnabled nestedScrollEnabled showsHorizontalScrollIndicator={false} showsVerticalScrollIndicator={false} contentContainerStyle={styles.imageScrollContainer} scrollEnabled={swipeToCloseEnabled} {...(swipeToCloseEnabled && {
110+ return (<ScrollView ref={imageContainer} style={{ width: SCREEN_WIDTH, height: SCREEN_HEIGHT }} pagingEnabled nestedScrollEnabled showsHorizontalScrollIndicator={false} showsVerticalScrollIndicator={false} contentContainerStyle={{ height: SCREEN_HEIGHT * 2 }} scrollEnabled={swipeToCloseEnabled} {...(swipeToCloseEnabled && {
111 onScroll,
112 onScrollEndDrag,
113 })}>
114- <Animated.Image {...panHandlers} source={imageSrc} style={imageStylesWithOpacity} onLoad={onLoaded}/>
115- {(!isLoaded || !imageDimensions) && <ImageLoading />}
116+ {/* Pinch from anywhere on screen, not only on the fitted image. */}
117+ <View {...panHandlers} pointerEvents={isLoaded && imageDimensions ? "auto" : "none"} style={{ width: SCREEN_WIDTH, height: SCREEN_HEIGHT }}>
118+ <Animated.Image source={imageSrc} style={imageStylesWithOpacity} onLoad={onLoaded}/>
119+ {(!isLoaded || !imageDimensions) && <ImageLoading />}
120+ </View>
121 </ScrollView>);
122 };
123-const styles = StyleSheet.create({
124- listItem: {
125- width: SCREEN_WIDTH,
126- height: SCREEN_HEIGHT,
127- },
128- imageScrollContainer: {
129- height: SCREEN_HEIGHT * 2,
130- },
131-});
132 export default React.memo(ImageItem);
133diff --git a/dist/components/ImageItem/ImageLoading.js b/dist/components/ImageItem/ImageLoading.js
134index 9770f7db158a2fb2b1c69a49e5fcbc89d8624603..995fab7524bb8673258d44e5eda361c62dae0324 100644
135--- a/dist/components/ImageItem/ImageLoading.js
136+++ b/dist/components/ImageItem/ImageLoading.js
137@@ -6,25 +6,17 @@
138 *
139 */
140 import React from "react";
141-import { ActivityIndicator, Dimensions, StyleSheet, View } from "react-native";
142+import { ActivityIndicator, Dimensions, Platform, StyleSheet, View } from "react-native";
143 const SCREEN = Dimensions.get("screen");
144-const SCREEN_WIDTH = SCREEN.width;
145-const SCREEN_HEIGHT = SCREEN.height;
146-export const ImageLoading = () => (<View style={styles.loading}>
147+export const ImageLoading = () => {
148+ const { width, height } = Platform.OS === "android" ? Dimensions.get("window") : SCREEN;
149+ return (<View style={[styles.loading, { width, height }]}>
150 <ActivityIndicator size="small" color="#FFF"/>
151 </View>);
152+};
153 const styles = StyleSheet.create({
154- listItem: {
155- width: SCREEN_WIDTH,
156- height: SCREEN_HEIGHT,
157- },
158 loading: {
159- width: SCREEN_WIDTH,
160- height: SCREEN_HEIGHT,
161 alignItems: "center",
162 justifyContent: "center",
163 },
164- imageScrollContainer: {
165- height: SCREEN_HEIGHT,
166- },
167 });
168diff --git a/dist/hooks/useAnimatedComponents.d.ts b/dist/hooks/useAnimatedComponents.d.ts
169index de5957458fea62f33743103acaa170a2c90d3638..dbafa8c5c5f3f86161e8520822cf10e19e88ea25 100644
170--- a/dist/hooks/useAnimatedComponents.d.ts
171+++ b/dist/hooks/useAnimatedComponents.d.ts
172@@ -6,7 +6,7 @@
173 *
174 */
175 import { Animated } from "react-native";
176-declare const useAnimatedComponents: () => readonly [{
177+declare const useAnimatedComponents: (resetKey?: string) => readonly [{
178 [key: string]: Animated.Value;
179 }[], {
180 [key: string]: Animated.Value;
181diff --git a/dist/hooks/useAnimatedComponents.js b/dist/hooks/useAnimatedComponents.js
182index 4730fe40735bd80afc97abdd14150a2fb4abf3e5..0ed2100744ccc9bcee7f1b15b0328a729d47bb16 100644
183--- a/dist/hooks/useAnimatedComponents.js
184+++ b/dist/hooks/useAnimatedComponents.js
185@@ -5,16 +5,19 @@
186 * LICENSE file in the root directory of this source tree.
187 *
188 */
189+import { useCallback, useMemo } from "react";
190 import { Animated } from "react-native";
191 const INITIAL_POSITION = { x: 0, y: 0 };
192 const ANIMATION_CONFIG = {
193 duration: 200,
194 useNativeDriver: true,
195 };
196-const useAnimatedComponents = () => {
197- const headerTranslate = new Animated.ValueXY(INITIAL_POSITION);
198- const footerTranslate = new Animated.ValueXY(INITIAL_POSITION);
199- const toggleVisible = (isVisible) => {
200+const useAnimatedComponents = (resetKey) => {
201+ const [headerTranslate, footerTranslate] = useMemo(() => [
202+ new Animated.ValueXY(INITIAL_POSITION),
203+ new Animated.ValueXY(INITIAL_POSITION),
204+ ], [resetKey]);
205+ const toggleVisible = useCallback((isVisible) => {
206 if (isVisible) {
207 Animated.parallel([
208 Animated.timing(headerTranslate.y, { ...ANIMATION_CONFIG, toValue: 0 }),
209@@ -33,7 +36,7 @@ const useAnimatedComponents = () => {
210 }),
211 ]).start();
212 }
213- };
214+ }, [headerTranslate, footerTranslate]);
215 const headerTransform = headerTranslate.getTranslateTransform();
216 const footerTransform = footerTranslate.getTranslateTransform();
217 return [headerTransform, footerTransform, toggleVisible];
218diff --git a/dist/hooks/useImageIndexChange.js b/dist/hooks/useImageIndexChange.js
219index cb5f0f798e5cfeedf459b7d22e92871db3092020..97ca2c9c326b1c53e6a5d90c3bb95d53342088d1 100644
220--- a/dist/hooks/useImageIndexChange.js
221+++ b/dist/hooks/useImageIndexChange.js
222@@ -5,16 +5,34 @@
223 * LICENSE file in the root directory of this source tree.
224 *
225 */
226-import { useState } from "react";
227+import { useCallback, useLayoutEffect, useRef, useState } from "react";
228+import { Platform } from "react-native";
229 const useImageIndexChange = (imageIndex, screen) => {
230 const [currentImageIndex, setImageIndex] = useState(imageIndex);
231- const onScroll = (event) => {
232+ const indexRef = useRef(imageIndex);
233+ const activeScroll = useRef(null);
234+ const onScroll = useCallback(function handleScroll(event) {
235+ if (Platform.OS === "android") {
236+ // Events at this width stay valid until commit; replaced or relaid-out lists do not.
237+ if (activeScroll.current !== handleScroll ||
238+ Math.abs(event.nativeEvent.layoutMeasurement.width - screen.width) > 1) {
239+ return;
240+ }
241+ }
242 const { nativeEvent: { contentOffset: { x: scrollX }, }, } = event;
243 if (screen.width) {
244 const nextIndex = Math.round(scrollX / screen.width);
245- setImageIndex(nextIndex < 0 ? 0 : nextIndex);
246+ const boundedIndex = nextIndex < 0 ? 0 : nextIndex;
247+ if (boundedIndex !== indexRef.current) {
248+ indexRef.current = boundedIndex;
249+ setImageIndex(boundedIndex);
250+ }
251 }
252- };
253+ }, [screen]);
254+ useLayoutEffect(() => {
255+ activeScroll.current = onScroll;
256+ return () => { activeScroll.current = null; };
257+ }, [onScroll]);
258 return [currentImageIndex, onScroll];
259 };
260 export default useImageIndexChange;
261diff --git a/dist/hooks/usePanResponder.js b/dist/hooks/usePanResponder.js
262index 99dcbc7efea4eba86a8bc2bad71dde73c8ebf1b4..7a7601eef4d330448d876b6460f53bd518a2c18c 100644
263--- a/dist/hooks/usePanResponder.js
264+++ b/dist/hooks/usePanResponder.js
265@@ -8,14 +8,14 @@
266 import { useMemo, useEffect } from "react";
267 import { Animated, Dimensions, } from "react-native";
268 import { createPanResponder, getDistanceBetweenTouches, getImageTranslate, getImageDimensionsByTranslate, } from "../utils";
269-const SCREEN = Dimensions.get("window");
270-const SCREEN_WIDTH = SCREEN.width;
271-const SCREEN_HEIGHT = SCREEN.height;
272-const MIN_DIMENSION = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT);
273 const SCALE_MAX = 2;
274 const DOUBLE_TAP_DELAY = 300;
275 const OUT_BOUND_MULTIPLIER = 0.75;
276 const usePanResponder = ({ initialScale, initialTranslate, onZoom, doubleTapToZoomEnabled, onLongPress, delayLongPress, }) => {
277+ const SCREEN = Dimensions.get("window");
278+ const SCREEN_WIDTH = SCREEN.width;
279+ const SCREEN_HEIGHT = SCREEN.height;
280+ const MIN_DIMENSION = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT);
281 let numberInitialTouches = 1;
282 let initialTouches = [];
283 let currentScale = initialScale;
284@@ -266,6 +266,9 @@ const usePanResponder = ({ initialScale, initialTranslate, onZoom, doubleTapToZo
285 tmpTranslate = null;
286 }
287 },
288+ // A gesture taken over mid-pinch must commit like a release, or pans stay
289+ // gated on the pre-pinch scale while the image still shows zoomed.
290+ onTerminate: () => handlers.onRelease(),
291 };
292 const panResponder = useMemo(() => createPanResponder(handlers), [handlers]);
293 return [panResponder.panHandlers, scaleValue, translateValue];