diff options
author | dan <dan.abramov@gmail.com> | 2024-11-06 00:21:35 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-11-06 00:21:35 +0000 |
commit | 206df2ab801d211a412f9ce3694d90bdd053caaa (patch) | |
tree | e185c0694eba262c48bd08fc0f430dd0fb203a47 /src/view/com/lightbox/ImageViewing/index.tsx | |
parent | 6b826fb88dd33fe594fd7bb631a90d1a1713d0df (diff) | |
download | voidsky-206df2ab801d211a412f9ce3694d90bdd053caaa.tar.zst |
Remove SCREEN from lightbox layout (#6124)
* Assign an ID to lightbox and use it as a key * Consolidate lightbox props into an object * Remove unused prop * Move SafeAreaView declaration * Keep SafeAreaView always mounted When exploring Android animation, I noticed its content jumps on the first frame. I think this should help prevent that. * Pass safe area down for measurement * Remove dependency on SCREEN in Android event handlers * Remove dependency on SCREEN in iOS event handlers * Remove dependency on SCREEN on iOS * Remove dependency on SCREEN on Android * Remove dependency on JS calc in controls * Use flex for iOS layout
Diffstat (limited to 'src/view/com/lightbox/ImageViewing/index.tsx')
-rw-r--r-- | src/view/com/lightbox/ImageViewing/index.tsx | 289 |
1 files changed, 157 insertions, 132 deletions
diff --git a/src/view/com/lightbox/ImageViewing/index.tsx b/src/view/com/lightbox/ImageViewing/index.tsx index 40df4c819..791701bca 100644 --- a/src/view/com/lightbox/ImageViewing/index.tsx +++ b/src/view/com/lightbox/ImageViewing/index.tsx @@ -8,24 +8,22 @@ // Original code copied and simplified from the link below as the codebase is currently not maintained: // https://github.com/jobtoday/react-native-image-viewing -import React, {useCallback, useMemo, useState} from 'react' -import { - Dimensions, - LayoutAnimation, - Platform, - StyleSheet, - View, -} from 'react-native' +import React, {useCallback, useState} from 'react' +import {LayoutAnimation, Platform, StyleSheet, View} from 'react-native' import PagerView from 'react-native-pager-view' -import {MeasuredDimensions} from 'react-native-reanimated' -import Animated, {useAnimatedStyle, withSpring} from 'react-native-reanimated' -import {useSafeAreaInsets} from 'react-native-safe-area-context' +import Animated, { + AnimatedRef, + useAnimatedRef, + useAnimatedStyle, + withSpring, +} from 'react-native-reanimated' import {Edge, SafeAreaView} from 'react-native-safe-area-context' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Trans} from '@lingui/macro' import {colors, s} from '#/lib/styles' import {isIOS} from '#/platform/detection' +import {Lightbox} from '#/state/lightbox' import {Button} from '#/view/com/util/forms/Button' import {Text} from '#/view/com/util/text/Text' import {ScrollView} from '#/view/com/util/Views' @@ -33,37 +31,68 @@ import {ImageSource} from './@types' import ImageDefaultHeader from './components/ImageDefaultHeader' import ImageItem from './components/ImageItem/ImageItem' -type Props = { - images: ImageSource[] - thumbDims: MeasuredDimensions | null - initialImageIndex: number - visible: boolean +const EDGES = + Platform.OS === 'android' + ? (['top', 'bottom', 'left', 'right'] satisfies Edge[]) + : (['left', 'right'] satisfies Edge[]) // iOS, so no top/bottom safe area + +export default function ImageViewRoot({ + lightbox, + onRequestClose, + onPressSave, + onPressShare, +}: { + lightbox: Lightbox | null onRequestClose: () => void - backgroundColor?: string onPressSave: (uri: string) => void onPressShare: (uri: string) => void +}) { + const ref = useAnimatedRef<View>() + return ( + // Keep it always mounted to avoid flicker on the first frame. + <SafeAreaView + style={[styles.screen, !lightbox && styles.screenHidden]} + edges={EDGES} + aria-modal + accessibilityViewIsModal + aria-hidden={!lightbox}> + <Animated.View ref={ref} style={{flex: 1}} collapsable={false}> + {lightbox && ( + <ImageView + key={lightbox.id} + lightbox={lightbox} + onRequestClose={onRequestClose} + onPressSave={onPressSave} + onPressShare={onPressShare} + safeAreaRef={ref} + /> + )} + </Animated.View> + </SafeAreaView> + ) } -const SCREEN_HEIGHT = Dimensions.get('window').height -const DEFAULT_BG_COLOR = '#000' - -function ImageViewing({ - images, - thumbDims: _thumbDims, // TODO: Pass down and use for animation. - initialImageIndex, - visible, +function ImageView({ + lightbox, onRequestClose, - backgroundColor = DEFAULT_BG_COLOR, onPressSave, onPressShare, -}: Props) { + safeAreaRef, +}: { + lightbox: Lightbox + onRequestClose: () => void + onPressSave: (uri: string) => void + onPressShare: (uri: string) => void + safeAreaRef: AnimatedRef<View> +}) { + const {images, index: initialImageIndex} = lightbox const [isScaled, setIsScaled] = useState(false) const [isDragging, setIsDragging] = useState(false) const [imageIndex, setImageIndex] = useState(initialImageIndex) const [showControls, setShowControls] = useState(true) const animatedHeaderStyle = useAnimatedStyle(() => ({ - pointerEvents: showControls ? 'auto' : 'none', + pointerEvents: showControls ? 'box-none' : 'none', opacity: withClampedSpring(showControls ? 1 : 0), transform: [ { @@ -72,7 +101,8 @@ function ImageViewing({ ], })) const animatedFooterStyle = useAnimatedStyle(() => ({ - pointerEvents: showControls ? 'auto' : 'none', + flexGrow: 1, + pointerEvents: showControls ? 'box-none' : 'none', opacity: withClampedSpring(showControls ? 1 : 0), transform: [ { @@ -92,53 +122,39 @@ function ImageViewing({ } }, []) - const edges = useMemo(() => { - if (Platform.OS === 'android') { - return ['top', 'bottom', 'left', 'right'] satisfies Edge[] - } - return ['left', 'right'] satisfies Edge[] // iOS, so no top/bottom safe area - }, []) - - if (!visible) { - return null - } - return ( - <SafeAreaView - style={styles.screen} - edges={edges} - aria-modal - accessibilityViewIsModal> - <View style={[styles.container, {backgroundColor}]}> - <Animated.View style={[styles.header, animatedHeaderStyle]}> + <View style={[styles.container]}> + <PagerView + scrollEnabled={!isScaled} + initialPage={initialImageIndex} + onPageSelected={e => { + setImageIndex(e.nativeEvent.position) + setIsScaled(false) + }} + onPageScrollStateChanged={e => { + setIsDragging(e.nativeEvent.pageScrollState !== 'idle') + }} + overdrag={true} + style={styles.pager}> + {images.map(imageSrc => ( + <View key={imageSrc.uri}> + <ImageItem + onTap={onTap} + onZoom={onZoom} + imageSrc={imageSrc} + onRequestClose={onRequestClose} + isScrollViewBeingDragged={isDragging} + showControls={showControls} + safeAreaRef={safeAreaRef} + /> + </View> + ))} + </PagerView> + <View style={styles.controls}> + <Animated.View style={animatedHeaderStyle}> <ImageDefaultHeader onRequestClose={onRequestClose} /> </Animated.View> - <PagerView - scrollEnabled={!isScaled} - initialPage={initialImageIndex} - onPageSelected={e => { - setImageIndex(e.nativeEvent.position) - setIsScaled(false) - }} - onPageScrollStateChanged={e => { - setIsDragging(e.nativeEvent.pageScrollState !== 'idle') - }} - overdrag={true} - style={styles.pager}> - {images.map(imageSrc => ( - <View key={imageSrc.uri}> - <ImageItem - onTap={onTap} - onZoom={onZoom} - imageSrc={imageSrc} - onRequestClose={onRequestClose} - isScrollViewBeingDragged={isDragging} - showControls={showControls} - /> - </View> - ))} - </PagerView> - <Animated.View style={[styles.footer, animatedFooterStyle]}> + <Animated.View style={animatedFooterStyle}> <LightboxFooter images={images} index={imageIndex} @@ -147,7 +163,7 @@ function ImageViewing({ /> </Animated.View> </View> - </SafeAreaView> + </View> ) } @@ -164,17 +180,10 @@ function LightboxFooter({ }) { const {alt: altText, uri} = images[index] const [isAltExpanded, setAltExpanded] = React.useState(false) - const insets = useSafeAreaInsets() - const svMaxHeight = SCREEN_HEIGHT - insets.top - 50 const isMomentumScrolling = React.useRef(false) return ( <ScrollView - style={[ - { - backgroundColor: '#000d', - }, - {maxHeight: svMaxHeight}, - ]} + style={styles.footerScrollView} scrollEnabled={isAltExpanded} onMomentumScrollBegin={() => { isMomentumScrolling.current = true @@ -183,51 +192,52 @@ function LightboxFooter({ isMomentumScrolling.current = false }} contentContainerStyle={{ - paddingTop: 16, - paddingBottom: insets.bottom + 10, + paddingVertical: 12, paddingHorizontal: 24, }}> - {altText ? ( - <View accessibilityRole="button" style={styles.footerText}> - <Text - style={[s.gray3]} - numberOfLines={isAltExpanded ? undefined : 3} - selectable - onPress={() => { - if (isMomentumScrolling.current) { - return - } - LayoutAnimation.configureNext({ - duration: 450, - update: {type: 'spring', springDamping: 1}, - }) - setAltExpanded(prev => !prev) - }} - onLongPress={() => {}}> - {altText} - </Text> + <SafeAreaView edges={['bottom']}> + {altText ? ( + <View accessibilityRole="button" style={styles.footerText}> + <Text + style={[s.gray3]} + numberOfLines={isAltExpanded ? undefined : 3} + selectable + onPress={() => { + if (isMomentumScrolling.current) { + return + } + LayoutAnimation.configureNext({ + duration: 450, + update: {type: 'spring', springDamping: 1}, + }) + setAltExpanded(prev => !prev) + }} + onLongPress={() => {}}> + {altText} + </Text> + </View> + ) : null} + <View style={styles.footerBtns}> + <Button + type="primary-outline" + style={styles.footerBtn} + onPress={() => onPressSave(uri)}> + <FontAwesomeIcon icon={['far', 'floppy-disk']} style={s.white} /> + <Text type="xl" style={s.white}> + <Trans context="action">Save</Trans> + </Text> + </Button> + <Button + type="primary-outline" + style={styles.footerBtn} + onPress={() => onPressShare(uri)}> + <FontAwesomeIcon icon="arrow-up-from-bracket" style={s.white} /> + <Text type="xl" style={s.white}> + <Trans context="action">Share</Trans> + </Text> + </Button> </View> - ) : null} - <View style={styles.footerBtns}> - <Button - type="primary-outline" - style={styles.footerBtn} - onPress={() => onPressSave(uri)}> - <FontAwesomeIcon icon={['far', 'floppy-disk']} style={s.white} /> - <Text type="xl" style={s.white}> - <Trans context="action">Save</Trans> - </Text> - </Button> - <Button - type="primary-outline" - style={styles.footerBtn} - onPress={() => onPressShare(uri)}> - <FontAwesomeIcon icon="arrow-up-from-bracket" style={s.white} /> - <Text type="xl" style={s.white}> - <Trans context="action">Share</Trans> - </Text> - </Button> - </View> + </SafeAreaView> </ScrollView> ) } @@ -240,25 +250,46 @@ const styles = StyleSheet.create({ bottom: 0, right: 0, }, + screenHidden: { + opacity: 0, + pointerEvents: 'none', + }, container: { flex: 1, backgroundColor: '#000', }, + controls: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + gap: 20, + zIndex: 1, + pointerEvents: 'box-none', + }, pager: { flex: 1, }, header: { position: 'absolute', width: '100%', - zIndex: 1, top: 0, pointerEvents: 'box-none', }, footer: { position: 'absolute', width: '100%', - zIndex: 1, + maxHeight: '100%', + bottom: 0, + }, + footerScrollView: { + backgroundColor: '#000d', + flex: 1, + position: 'absolute', bottom: 0, + width: '100%', + maxHeight: '100%', }, footerText: { paddingBottom: isIOS ? 20 : 16, @@ -277,13 +308,7 @@ const styles = StyleSheet.create({ }, }) -const EnhancedImageViewing = (props: Props) => ( - <ImageViewing key={props.initialImageIndex} {...props} /> -) - function withClampedSpring(value: any) { 'worklet' return withSpring(value, {overshootClamping: true, stiffness: 300}) } - -export default EnhancedImageViewing |