Change lightbox to use Pager (#1666)
* Change lightbox to use Pager * Fix crash issue on ios --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com>zio/stable
parent
aa085b0b14
commit
209d8b683c
|
@ -1,9 +1,8 @@
|
||||||
import React, {MutableRefObject, useState} from 'react'
|
import React, {useState} from 'react'
|
||||||
|
|
||||||
import {ActivityIndicator, Dimensions, StyleSheet} from 'react-native'
|
import {ActivityIndicator, Dimensions, StyleSheet} from 'react-native'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
measure,
|
|
||||||
runOnJS,
|
runOnJS,
|
||||||
useAnimatedRef,
|
useAnimatedRef,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
|
@ -12,11 +11,7 @@ import Animated, {
|
||||||
withDecay,
|
withDecay,
|
||||||
withSpring,
|
withSpring,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {
|
import {GestureDetector, Gesture} from 'react-native-gesture-handler'
|
||||||
GestureDetector,
|
|
||||||
Gesture,
|
|
||||||
GestureType,
|
|
||||||
} from 'react-native-gesture-handler'
|
|
||||||
import useImageDimensions from '../../hooks/useImageDimensions'
|
import useImageDimensions from '../../hooks/useImageDimensions'
|
||||||
import {
|
import {
|
||||||
createTransform,
|
createTransform,
|
||||||
|
@ -40,7 +35,6 @@ type Props = {
|
||||||
imageSrc: ImageSource
|
imageSrc: ImageSource
|
||||||
onRequestClose: () => void
|
onRequestClose: () => void
|
||||||
onZoom: (isZoomed: boolean) => void
|
onZoom: (isZoomed: boolean) => void
|
||||||
pinchGestureRef: MutableRefObject<GestureType | undefined>
|
|
||||||
isScrollViewBeingDragged: boolean
|
isScrollViewBeingDragged: boolean
|
||||||
}
|
}
|
||||||
const ImageItem = ({
|
const ImageItem = ({
|
||||||
|
@ -48,7 +42,6 @@ const ImageItem = ({
|
||||||
onZoom,
|
onZoom,
|
||||||
onRequestClose,
|
onRequestClose,
|
||||||
isScrollViewBeingDragged,
|
isScrollViewBeingDragged,
|
||||||
pinchGestureRef,
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isScaled, setIsScaled] = useState(false)
|
const [isScaled, setIsScaled] = useState(false)
|
||||||
const [isLoaded, setIsLoaded] = useState(false)
|
const [isLoaded, setIsLoaded] = useState(false)
|
||||||
|
@ -140,28 +133,7 @@ const ImageItem = ({
|
||||||
return [dx, dy]
|
return [dx, dy]
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a hack.
|
|
||||||
// We need to disallow any gestures (and let the native parent scroll view scroll) while you're scrolling it.
|
|
||||||
// However, there is no great reliable way to coordinate this yet in RGNH.
|
|
||||||
// This "fake" manual gesture handler whenever you're trying to touch something while the parent scrollview is not at rest.
|
|
||||||
const consumeHScroll = Gesture.Manual().onTouchesDown((e, manager) => {
|
|
||||||
if (isScrollViewBeingDragged) {
|
|
||||||
// Steal the gesture (and do nothing, so native ScrollView does its thing).
|
|
||||||
manager.activate()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const measurement = measure(containerRef)
|
|
||||||
if (!measurement || measurement.pageX !== 0) {
|
|
||||||
// Steal the gesture (and do nothing, so native ScrollView does its thing).
|
|
||||||
manager.activate()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Fail this "fake" gesture so that the gestures after it can proceed.
|
|
||||||
manager.fail()
|
|
||||||
})
|
|
||||||
|
|
||||||
const pinch = Gesture.Pinch()
|
const pinch = Gesture.Pinch()
|
||||||
.withRef(pinchGestureRef)
|
|
||||||
.onStart(e => {
|
.onStart(e => {
|
||||||
pinchOrigin.value = {
|
pinchOrigin.value = {
|
||||||
x: e.focalX - SCREEN.width / 2,
|
x: e.focalX - SCREEN.width / 2,
|
||||||
|
@ -318,19 +290,22 @@ const ImageItem = ({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const composedGesture = isScrollViewBeingDragged
|
||||||
|
? // If the parent is not at rest, provide a no-op gesture.
|
||||||
|
Gesture.Manual()
|
||||||
|
: Gesture.Exclusive(
|
||||||
|
dismissSwipePan,
|
||||||
|
Gesture.Simultaneous(pinch, pan),
|
||||||
|
doubleTap,
|
||||||
|
)
|
||||||
|
|
||||||
const isLoading = !isLoaded || !imageDimensions
|
const isLoading = !isLoaded || !imageDimensions
|
||||||
return (
|
return (
|
||||||
<Animated.View ref={containerRef} style={styles.container}>
|
<Animated.View ref={containerRef} style={styles.container}>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<ActivityIndicator size="small" color="#FFF" style={styles.loading} />
|
<ActivityIndicator size="small" color="#FFF" style={styles.loading} />
|
||||||
)}
|
)}
|
||||||
<GestureDetector
|
<GestureDetector gesture={composedGesture}>
|
||||||
gesture={Gesture.Exclusive(
|
|
||||||
consumeHScroll,
|
|
||||||
dismissSwipePan,
|
|
||||||
Gesture.Simultaneous(pinch, pan),
|
|
||||||
doubleTap,
|
|
||||||
)}>
|
|
||||||
<AnimatedImage
|
<AnimatedImage
|
||||||
source={imageSrc}
|
source={imageSrc}
|
||||||
contentFit="contain"
|
contentFit="contain"
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {MutableRefObject, useCallback, useState} from 'react'
|
import React, {useCallback, useState} from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
|
@ -25,7 +25,6 @@ import Animated, {
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {GestureType} from 'react-native-gesture-handler'
|
|
||||||
|
|
||||||
import useImageDimensions from '../../hooks/useImageDimensions'
|
import useImageDimensions from '../../hooks/useImageDimensions'
|
||||||
|
|
||||||
|
@ -43,7 +42,6 @@ type Props = {
|
||||||
imageSrc: ImageSource
|
imageSrc: ImageSource
|
||||||
onRequestClose: () => void
|
onRequestClose: () => void
|
||||||
onZoom: (scaled: boolean) => void
|
onZoom: (scaled: boolean) => void
|
||||||
pinchGestureRef: MutableRefObject<GestureType>
|
|
||||||
isScrollViewBeingDragged: boolean
|
isScrollViewBeingDragged: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +143,7 @@ const ImageItem = ({imageSrc, onZoom, onRequestClose}: Props) => {
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<AnimatedImage
|
<AnimatedImage
|
||||||
contentFit="contain"
|
contentFit="contain"
|
||||||
source={imageSrc}
|
source={{uri: imageSrc.uri}}
|
||||||
style={[styles.image, animatedStyle]}
|
style={[styles.image, animatedStyle]}
|
||||||
onLoad={() => setLoaded(true)}
|
onLoad={() => setLoaded(true)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
// default implementation fallback for web
|
// default implementation fallback for web
|
||||||
|
|
||||||
import React, {MutableRefObject} from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {GestureType} from 'react-native-gesture-handler'
|
|
||||||
import {ImageSource} from '../../@types'
|
import {ImageSource} from '../../@types'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
imageSrc: ImageSource
|
imageSrc: ImageSource
|
||||||
onRequestClose: () => void
|
onRequestClose: () => void
|
||||||
onZoom: (scaled: boolean) => void
|
onZoom: (scaled: boolean) => void
|
||||||
pinchGestureRef: MutableRefObject<GestureType | undefined>
|
|
||||||
isScrollViewBeingDragged: boolean
|
isScrollViewBeingDragged: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,32 +8,15 @@
|
||||||
// Original code copied and simplified from the link below as the codebase is currently not maintained:
|
// Original code copied and simplified from the link below as the codebase is currently not maintained:
|
||||||
// https://github.com/jobtoday/react-native-image-viewing
|
// https://github.com/jobtoday/react-native-image-viewing
|
||||||
|
|
||||||
import React, {
|
import React, {ComponentType, useMemo, useState} from 'react'
|
||||||
ComponentType,
|
import {Animated, StyleSheet, View, ModalProps, Platform} from 'react-native'
|
||||||
createRef,
|
|
||||||
useCallback,
|
|
||||||
useRef,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react'
|
|
||||||
import {
|
|
||||||
Animated,
|
|
||||||
Dimensions,
|
|
||||||
NativeSyntheticEvent,
|
|
||||||
NativeScrollEvent,
|
|
||||||
StyleSheet,
|
|
||||||
View,
|
|
||||||
VirtualizedList,
|
|
||||||
ModalProps,
|
|
||||||
Platform,
|
|
||||||
} from 'react-native'
|
|
||||||
|
|
||||||
import ImageItem from './components/ImageItem/ImageItem'
|
import ImageItem from './components/ImageItem/ImageItem'
|
||||||
import ImageDefaultHeader from './components/ImageDefaultHeader'
|
import ImageDefaultHeader from './components/ImageDefaultHeader'
|
||||||
|
|
||||||
import {ImageSource} from './@types'
|
import {ImageSource} from './@types'
|
||||||
import {ScrollView, GestureType} from 'react-native-gesture-handler'
|
|
||||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context'
|
import {Edge, SafeAreaView} from 'react-native-safe-area-context'
|
||||||
|
import PagerView from 'react-native-pager-view'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
images: ImageSource[]
|
images: ImageSource[]
|
||||||
|
@ -48,8 +31,6 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_BG_COLOR = '#000'
|
const DEFAULT_BG_COLOR = '#000'
|
||||||
const SCREEN = Dimensions.get('screen')
|
|
||||||
const SCREEN_WIDTH = SCREEN.width
|
|
||||||
const INITIAL_POSITION = {x: 0, y: 0}
|
const INITIAL_POSITION = {x: 0, y: 0}
|
||||||
const ANIMATION_CONFIG = {
|
const ANIMATION_CONFIG = {
|
||||||
duration: 200,
|
duration: 200,
|
||||||
|
@ -65,7 +46,6 @@ function ImageViewing({
|
||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
FooterComponent,
|
FooterComponent,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const imageList = useRef<VirtualizedList<ImageSource>>(null)
|
|
||||||
const [isScaled, setIsScaled] = useState(false)
|
const [isScaled, setIsScaled] = useState(false)
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const [imageIndex, setImageIndex] = useState(initialImageIndex)
|
const [imageIndex, setImageIndex] = useState(initialImageIndex)
|
||||||
|
@ -96,19 +76,6 @@ function ImageViewing({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
||||||
const {
|
|
||||||
nativeEvent: {
|
|
||||||
contentOffset: {x: scrollX},
|
|
||||||
},
|
|
||||||
} = event
|
|
||||||
|
|
||||||
if (SCREEN.width) {
|
|
||||||
const nextIndex = Math.round(scrollX / SCREEN.width)
|
|
||||||
setImageIndex(nextIndex < 0 ? 0 : nextIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onZoom = (nextIsScaled: boolean) => {
|
const onZoom = (nextIsScaled: boolean) => {
|
||||||
toggleBarsVisible(!nextIsScaled)
|
toggleBarsVisible(!nextIsScaled)
|
||||||
setIsScaled(false)
|
setIsScaled(false)
|
||||||
|
@ -121,26 +88,6 @@ function ImageViewing({
|
||||||
return ['left', 'right'] satisfies Edge[] // iOS, so no top/bottom safe area
|
return ['left', 'right'] satisfies Edge[] // iOS, so no top/bottom safe area
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onLayout = useCallback(() => {
|
|
||||||
if (initialImageIndex) {
|
|
||||||
imageList.current?.scrollToIndex({
|
|
||||||
index: initialImageIndex,
|
|
||||||
animated: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [imageList, initialImageIndex])
|
|
||||||
|
|
||||||
// This is a hack.
|
|
||||||
// RNGH doesn't have an easy way to express that pinch of individual items
|
|
||||||
// should "steal" all pinches from the scroll view. So we're keeping a ref
|
|
||||||
// to all pinch gestures so that we may give them to <ScrollView waitFor={...}>.
|
|
||||||
const [pinchGestureRefs] = useState(new Map())
|
|
||||||
for (let imageSrc of images) {
|
|
||||||
if (!pinchGestureRefs.get(imageSrc)) {
|
|
||||||
pinchGestureRefs.set(imageSrc, createRef<GestureType | undefined>())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -150,7 +97,6 @@ function ImageViewing({
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
style={styles.screen}
|
style={styles.screen}
|
||||||
onLayout={onLayout}
|
|
||||||
edges={edges}
|
edges={edges}
|
||||||
aria-modal
|
aria-modal
|
||||||
accessibilityViewIsModal>
|
accessibilityViewIsModal>
|
||||||
|
@ -164,48 +110,29 @@ function ImageViewing({
|
||||||
<ImageDefaultHeader onRequestClose={onRequestClose} />
|
<ImageDefaultHeader onRequestClose={onRequestClose} />
|
||||||
)}
|
)}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
<VirtualizedList
|
<PagerView
|
||||||
ref={imageList}
|
scrollEnabled={!isScaled}
|
||||||
data={images}
|
initialPage={initialImageIndex}
|
||||||
horizontal
|
onPageSelected={e => {
|
||||||
pagingEnabled
|
setImageIndex(e.nativeEvent.position)
|
||||||
scrollEnabled={!isScaled || isDragging}
|
setIsScaled(false)
|
||||||
showsHorizontalScrollIndicator={false}
|
}}
|
||||||
showsVerticalScrollIndicator={false}
|
onPageScrollStateChanged={e => {
|
||||||
getItem={(_, index) => images[index]}
|
setIsDragging(e.nativeEvent.pageScrollState !== 'idle')
|
||||||
getItemCount={() => images.length}
|
}}
|
||||||
getItemLayout={(_, index) => ({
|
overdrag={true}
|
||||||
length: SCREEN_WIDTH,
|
style={styles.pager}>
|
||||||
offset: SCREEN_WIDTH * index,
|
{images.map(imageSrc => (
|
||||||
index,
|
<View key={imageSrc.uri}>
|
||||||
})}
|
|
||||||
renderItem={({item: imageSrc}) => (
|
|
||||||
<ImageItem
|
<ImageItem
|
||||||
onZoom={onZoom}
|
onZoom={onZoom}
|
||||||
imageSrc={imageSrc}
|
imageSrc={imageSrc}
|
||||||
onRequestClose={onRequestClose}
|
onRequestClose={onRequestClose}
|
||||||
pinchGestureRef={pinchGestureRefs.get(imageSrc)}
|
|
||||||
isScrollViewBeingDragged={isDragging}
|
isScrollViewBeingDragged={isDragging}
|
||||||
/>
|
/>
|
||||||
)}
|
</View>
|
||||||
renderScrollComponent={props => (
|
))}
|
||||||
<ScrollView
|
</PagerView>
|
||||||
{...props}
|
|
||||||
waitFor={Array.from(pinchGestureRefs.values())}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
onScrollBeginDrag={() => {
|
|
||||||
setIsDragging(true)
|
|
||||||
}}
|
|
||||||
onScrollEndDrag={() => {
|
|
||||||
setIsDragging(false)
|
|
||||||
}}
|
|
||||||
onMomentumScrollEnd={e => {
|
|
||||||
setIsScaled(false)
|
|
||||||
onScroll(e)
|
|
||||||
}}
|
|
||||||
keyExtractor={imageSrc => imageSrc.uri}
|
|
||||||
/>
|
|
||||||
{typeof FooterComponent !== 'undefined' && (
|
{typeof FooterComponent !== 'undefined' && (
|
||||||
<Animated.View style={[styles.footer, {transform: footerTransform}]}>
|
<Animated.View style={[styles.footer, {transform: footerTransform}]}>
|
||||||
{React.createElement(FooterComponent, {
|
{React.createElement(FooterComponent, {
|
||||||
|
@ -221,11 +148,18 @@ function ImageViewing({
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
screen: {
|
screen: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#000',
|
backgroundColor: '#000',
|
||||||
},
|
},
|
||||||
|
pager: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
header: {
|
header: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
Loading…
Reference in New Issue