Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Toggle lightbox controls on tap (#1687)

* Make the lightbox controls animation smoother

* Toggle controls on tap

* Disable pointer events when hidden

authored by

dan and committed by
GitHub
abfd9a8c f447eaa6

+81 -63
+7
src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
··· 34 34 type Props = { 35 35 imageSrc: ImageSource 36 36 onRequestClose: () => void 37 + onTap: () => void 37 38 onZoom: (isZoomed: boolean) => void 38 39 isScrollViewBeingDragged: boolean 39 40 } 40 41 const ImageItem = ({ 41 42 imageSrc, 43 + onTap, 42 44 onZoom, 43 45 onRequestClose, 44 46 isScrollViewBeingDragged, ··· 227 229 panTranslation.value = {x: 0, y: 0} 228 230 }) 229 231 232 + const singleTap = Gesture.Tap().onEnd(() => { 233 + runOnJS(onTap)() 234 + }) 235 + 230 236 const doubleTap = Gesture.Tap() 231 237 .numberOfTaps(2) 232 238 .onEnd(e => { ··· 297 303 dismissSwipePan, 298 304 Gesture.Simultaneous(pinch, pan), 299 305 doubleTap, 306 + singleTap, 300 307 ) 301 308 302 309 const isLoading = !isLoaded || !imageDimensions
+57 -60
src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
··· 6 6 * 7 7 */ 8 8 9 - import React, {useCallback, useState} from 'react' 9 + import React, {useState} from 'react' 10 10 11 - import { 12 - Dimensions, 13 - StyleSheet, 14 - View, 15 - NativeSyntheticEvent, 16 - NativeTouchEvent, 17 - TouchableWithoutFeedback, 18 - } from 'react-native' 11 + import {Dimensions, StyleSheet} from 'react-native' 19 12 import {Image} from 'expo-image' 20 13 import Animated, { 21 14 interpolate, ··· 25 18 useAnimatedStyle, 26 19 useSharedValue, 27 20 } from 'react-native-reanimated' 21 + import {Gesture, GestureDetector} from 'react-native-gesture-handler' 28 22 29 23 import useImageDimensions from '../../hooks/useImageDimensions' 30 24 31 25 import {ImageSource, Dimensions as ImageDimensions} from '../../@types' 32 26 import {ImageLoading} from './ImageLoading' 33 27 34 - const DOUBLE_TAP_DELAY = 300 35 28 const SWIPE_CLOSE_OFFSET = 75 36 29 const SWIPE_CLOSE_VELOCITY = 1 37 30 const SCREEN = Dimensions.get('screen') ··· 41 34 type Props = { 42 35 imageSrc: ImageSource 43 36 onRequestClose: () => void 37 + onTap: () => void 44 38 onZoom: (scaled: boolean) => void 45 39 isScrollViewBeingDragged: boolean 46 40 } 47 41 48 42 const AnimatedImage = Animated.createAnimatedComponent(Image) 49 43 50 - let lastTapTS: number | null = null 51 - 52 - const ImageItem = ({imageSrc, onZoom, onRequestClose}: Props) => { 44 + const ImageItem = ({imageSrc, onTap, onZoom, onRequestClose}: Props) => { 53 45 const scrollViewRef = useAnimatedRef<Animated.ScrollView>() 54 46 const translationY = useSharedValue(0) 55 47 const [loaded, setLoaded] = useState(false) ··· 71 63 72 64 const scrollHandler = useAnimatedScrollHandler({ 73 65 onScroll(e) { 74 - translationY.value = e.zoomScale > 1 ? 0 : e.contentOffset.y 66 + const nextIsScaled = e.zoomScale > 1 67 + translationY.value = nextIsScaled ? 0 : e.contentOffset.y 68 + if (scaled !== nextIsScaled) { 69 + runOnJS(handleZoom)(nextIsScaled) 70 + } 75 71 }, 76 72 onEndDrag(e) { 77 73 const velocityY = e.velocity?.y ?? 0 78 74 const nextIsScaled = e.zoomScale > 1 79 - runOnJS(handleZoom)(nextIsScaled) 75 + if (scaled !== nextIsScaled) { 76 + runOnJS(handleZoom)(nextIsScaled) 77 + } 80 78 if (!nextIsScaled && Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY) { 81 79 runOnJS(onRequestClose)() 82 80 } ··· 88 86 setScaled(nextIsScaled) 89 87 } 90 88 91 - const handleDoubleTap = useCallback( 92 - (event: NativeSyntheticEvent<NativeTouchEvent>) => { 93 - const nowTS = new Date().getTime() 94 - const scrollResponderRef = scrollViewRef?.current?.getScrollResponder() 89 + function handleDoubleTap(absoluteX: number, absoluteY: number) { 90 + const scrollResponderRef = scrollViewRef?.current?.getScrollResponder() 91 + let nextZoomRect = { 92 + x: 0, 93 + y: 0, 94 + width: SCREEN.width, 95 + height: SCREEN.height, 96 + } 97 + 98 + const willZoom = !scaled 99 + if (willZoom) { 100 + nextZoomRect = getZoomRectAfterDoubleTap( 101 + imageDimensions, 102 + absoluteX, 103 + absoluteY, 104 + ) 105 + } 95 106 96 - if (lastTapTS && nowTS - lastTapTS < DOUBLE_TAP_DELAY) { 97 - let nextZoomRect = { 98 - x: 0, 99 - y: 0, 100 - width: SCREEN.width, 101 - height: SCREEN.height, 102 - } 107 + // @ts-ignore 108 + scrollResponderRef?.scrollResponderZoomTo({ 109 + ...nextZoomRect, // This rect is in screen coordinates 110 + animated: true, 111 + }) 112 + } 113 + 114 + const singleTap = Gesture.Tap().onEnd(() => { 115 + runOnJS(onTap)() 116 + }) 103 117 104 - const willZoom = !scaled 105 - if (willZoom) { 106 - const {pageX, pageY} = event.nativeEvent 107 - nextZoomRect = getZoomRectAfterDoubleTap( 108 - imageDimensions, 109 - pageX, 110 - pageY, 111 - ) 112 - } 118 + const doubleTap = Gesture.Tap() 119 + .numberOfTaps(2) 120 + .onEnd(e => { 121 + const {absoluteX, absoluteY} = e 122 + runOnJS(handleDoubleTap)(absoluteX, absoluteY) 123 + }) 113 124 114 - // @ts-ignore 115 - scrollResponderRef?.scrollResponderZoomTo({ 116 - ...nextZoomRect, // This rect is in screen coordinates 117 - animated: true, 118 - }) 119 - } else { 120 - lastTapTS = nowTS 121 - } 122 - }, 123 - [imageDimensions, scaled, scrollViewRef], 124 - ) 125 + const composedGesture = Gesture.Exclusive(doubleTap, singleTap) 125 126 126 127 return ( 127 - <View> 128 + <GestureDetector gesture={composedGesture}> 128 129 <Animated.ScrollView 129 130 // @ts-ignore Something's up with the types here 130 131 ref={scrollViewRef} ··· 136 137 contentContainerStyle={styles.imageScrollContainer} 137 138 onScroll={scrollHandler}> 138 139 {(!loaded || !imageDimensions) && <ImageLoading />} 139 - <TouchableWithoutFeedback 140 - onPress={handleDoubleTap} 141 - accessibilityRole="image" 140 + <AnimatedImage 141 + contentFit="contain" 142 + // NOTE: Don't pass imageSrc={imageSrc} or MobX will break. 143 + source={{uri: imageSrc.uri}} 144 + style={[styles.image, animatedStyle]} 142 145 accessibilityLabel={imageSrc.alt} 143 - accessibilityHint=""> 144 - <AnimatedImage 145 - contentFit="contain" 146 - // NOTE: Don't pass imageSrc={imageSrc} or MobX will break. 147 - source={{uri: imageSrc.uri}} 148 - style={[styles.image, animatedStyle]} 149 - onLoad={() => setLoaded(true)} 150 - /> 151 - </TouchableWithoutFeedback> 146 + accessibilityHint="" 147 + onLoad={() => setLoaded(true)} 148 + /> 152 149 </Animated.ScrollView> 153 - </View> 150 + </GestureDetector> 154 151 ) 155 152 } 156 153
+1
src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx
··· 7 7 type Props = { 8 8 imageSrc: ImageSource 9 9 onRequestClose: () => void 10 + onTap: () => void 10 11 onZoom: (scaled: boolean) => void 11 12 isScrollViewBeingDragged: boolean 12 13 }
+16 -3
src/view/com/lightbox/ImageViewing/index.tsx
··· 43 43 const [isScaled, setIsScaled] = useState(false) 44 44 const [isDragging, setIsDragging] = useState(false) 45 45 const [imageIndex, setImageIndex] = useState(initialImageIndex) 46 + const [showControls, setShowControls] = useState(true) 46 47 47 48 const animatedHeaderStyle = useAnimatedStyle(() => ({ 49 + pointerEvents: showControls ? 'auto' : 'none', 50 + opacity: withClampedSpring(showControls ? 1 : 0), 48 51 transform: [ 49 52 { 50 - translateY: withClampedSpring(isScaled ? -300 : 0), 53 + translateY: withClampedSpring(showControls ? 0 : -30), 51 54 }, 52 55 ], 53 56 })) 54 57 const animatedFooterStyle = useAnimatedStyle(() => ({ 58 + pointerEvents: showControls ? 'auto' : 'none', 59 + opacity: withClampedSpring(showControls ? 1 : 0), 55 60 transform: [ 56 61 { 57 - translateY: withClampedSpring(isScaled ? 300 : 0), 62 + translateY: withClampedSpring(showControls ? 0 : 30), 58 63 }, 59 64 ], 60 65 })) 61 66 67 + const onTap = useCallback(() => { 68 + setShowControls(show => !show) 69 + }, []) 70 + 62 71 const onZoom = useCallback((nextIsScaled: boolean) => { 63 72 setIsScaled(nextIsScaled) 73 + if (nextIsScaled) { 74 + setShowControls(false) 75 + } 64 76 }, []) 65 77 66 78 const edges = useMemo(() => { ··· 105 117 {images.map(imageSrc => ( 106 118 <View key={imageSrc.uri}> 107 119 <ImageItem 120 + onTap={onTap} 108 121 onZoom={onZoom} 109 122 imageSrc={imageSrc} 110 123 onRequestClose={onRequestClose} ··· 161 174 162 175 function withClampedSpring(value: any) { 163 176 'worklet' 164 - return withSpring(value, {overshootClamping: true}) 177 + return withSpring(value, {overshootClamping: true, stiffness: 300}) 165 178 } 166 179 167 180 export default EnhancedImageViewing