diff options
Diffstat (limited to 'src/view/com/lightbox/ImageViewing')
4 files changed, 267 insertions, 215 deletions
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx index 487acf931..ea77ec273 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx @@ -1,7 +1,9 @@ import React, {useState} from 'react' -import {ActivityIndicator, Dimensions, StyleSheet} from 'react-native' +import {ActivityIndicator, StyleSheet, View} from 'react-native' import {Gesture, GestureDetector} from 'react-native-gesture-handler' import Animated, { + AnimatedRef, + measure, runOnJS, useAnimatedReaction, useAnimatedRef, @@ -24,13 +26,6 @@ import { TransformMatrix, } from '../../transforms' -const windowDim = Dimensions.get('window') -const screenDim = Dimensions.get('screen') -const statusBarHeight = windowDim.height - screenDim.height -const SCREEN = { - width: windowDim.width, - height: windowDim.height + statusBarHeight, -} const MIN_DOUBLE_TAP_SCALE = 2 const MAX_ORIGINAL_IMAGE_ZOOM = 2 @@ -43,6 +38,7 @@ type Props = { onZoom: (isZoomed: boolean) => void isScrollViewBeingDragged: boolean showControls: boolean + safeAreaRef: AnimatedRef<View> } const ImageItem = ({ imageSrc, @@ -50,6 +46,7 @@ const ImageItem = ({ onZoom, onRequestClose, isScrollViewBeingDragged, + safeAreaRef, }: Props) => { const [isScaled, setIsScaled] = useState(false) const [imageAspect, imageDimensions] = useImageDimensions({ @@ -102,10 +99,10 @@ const ImageItem = ({ const [translateX, translateY, scale] = readTransform(t) const dismissDistance = dismissSwipeTranslateY.value - const dismissProgress = Math.min( - Math.abs(dismissDistance) / (SCREEN.height / 2), - 1, - ) + const screenSize = measure(safeAreaRef) + const dismissProgress = screenSize + ? Math.min(Math.abs(dismissDistance) / (screenSize.height / 2), 1) + : 0 return { opacity: 1 - dismissProgress, transform: [ @@ -120,6 +117,7 @@ const ImageItem = ({ // If the user tried to pan too hard, this function will provide the negative panning to stay in bounds. function getExtraTranslationToStayInBounds( candidateTransform: TransformMatrix, + screenSize: {width: number; height: number}, ) { 'worklet' if (!imageAspect) { @@ -127,16 +125,20 @@ const ImageItem = ({ } const [nextTranslateX, nextTranslateY, nextScale] = readTransform(candidateTransform) - const scaledDimensions = getScaledDimensions(imageAspect, nextScale) + const scaledDimensions = getScaledDimensions( + imageAspect, + nextScale, + screenSize, + ) const clampedTranslateX = clampTranslation( nextTranslateX, scaledDimensions.width, - SCREEN.width, + screenSize.width, ) const clampedTranslateY = clampTranslation( nextTranslateY, scaledDimensions.height, - SCREEN.height, + screenSize.height, ) const dx = clampedTranslateX - nextTranslateX const dy = clampedTranslateY - nextTranslateY @@ -146,21 +148,26 @@ const ImageItem = ({ const pinch = Gesture.Pinch() .onStart(e => { 'worklet' + const screenSize = measure(safeAreaRef) + if (!screenSize) { + return + } pinchOrigin.value = { - x: e.focalX - SCREEN.width / 2, - y: e.focalY - SCREEN.height / 2, + x: e.focalX - screenSize.width / 2, + y: e.focalY - screenSize.height / 2, } }) .onChange(e => { 'worklet' - if (!imageDimensions) { + const screenSize = measure(safeAreaRef) + if (!imageDimensions || !screenSize) { return } // Don't let the picture zoom in so close that it gets blurry. // Also, like in stock Android apps, don't let the user zoom out further than 1:1. const [, , committedScale] = readTransform(committedTransform.value) const maxCommittedScale = - (imageDimensions.width / SCREEN.width) * MAX_ORIGINAL_IMAGE_ZOOM + (imageDimensions.width / screenSize.width) * MAX_ORIGINAL_IMAGE_ZOOM const minPinchScale = 1 / committedScale const maxPinchScale = maxCommittedScale / committedScale const nextPinchScale = Math.min( @@ -175,7 +182,7 @@ const ImageItem = ({ prependPan(t, panTranslation.value) prependPinch(t, nextPinchScale, pinchOrigin.value, pinchTranslation.value) prependTransform(t, committedTransform.value) - const [dx, dy] = getExtraTranslationToStayInBounds(t) + const [dx, dy] = getExtraTranslationToStayInBounds(t, screenSize) if (dx !== 0 || dy !== 0) { pinchTranslation.value = { x: pinchTranslation.value.x + dx, @@ -209,9 +216,11 @@ const ImageItem = ({ .minPointers(isScaled ? 1 : 2) .onChange(e => { 'worklet' - if (!imageDimensions) { + const screenSize = measure(safeAreaRef) + if (!imageDimensions || !screenSize) { return } + const nextPanTranslation = {x: e.translationX, y: e.translationY} let t = createTransform() prependPan(t, nextPanTranslation) @@ -224,7 +233,7 @@ const ImageItem = ({ prependTransform(t, committedTransform.value) // Prevent panning from going out of bounds. - const [dx, dy] = getExtraTranslationToStayInBounds(t) + const [dx, dy] = getExtraTranslationToStayInBounds(t, screenSize) nextPanTranslation.x += dx nextPanTranslation.y += dy panTranslation.value = nextPanTranslation @@ -251,7 +260,8 @@ const ImageItem = ({ .numberOfTaps(2) .onEnd(e => { 'worklet' - if (!imageDimensions || !imageAspect) { + const screenSize = measure(safeAreaRef) + if (!imageDimensions || !imageAspect || !screenSize) { return } const [, , committedScale] = readTransform(committedTransform.value) @@ -263,7 +273,7 @@ const ImageItem = ({ } // Try to zoom in so that we get rid of the black bars (whatever the orientation was). - const screenAspect = SCREEN.width / SCREEN.height + const screenAspect = screenSize.width / screenSize.height const candidateScale = Math.max( imageAspect / screenAspect, screenAspect / imageAspect, @@ -271,20 +281,23 @@ const ImageItem = ({ ) // But don't zoom in so close that the picture gets blurry. const maxScale = - (imageDimensions.width / SCREEN.width) * MAX_ORIGINAL_IMAGE_ZOOM + (imageDimensions.width / screenSize.width) * MAX_ORIGINAL_IMAGE_ZOOM const scale = Math.min(candidateScale, maxScale) // Calculate where we would be if the user pinched into the double tapped point. // We won't use this transform directly because it may go out of bounds. const candidateTransform = createTransform() const origin = { - x: e.absoluteX - SCREEN.width / 2, - y: e.absoluteY - SCREEN.height / 2, + x: e.absoluteX - screenSize.width / 2, + y: e.absoluteY - screenSize.height / 2, } prependPinch(candidateTransform, scale, origin, {x: 0, y: 0}) // Now we know how much we went out of bounds, so we can shoot correctly. - const [dx, dy] = getExtraTranslationToStayInBounds(candidateTransform) + const [dx, dy] = getExtraTranslationToStayInBounds( + candidateTransform, + screenSize, + ) const finalTransform = createTransform() prependPinch(finalTransform, scale, origin, {x: dx, y: dy}) committedTransform.value = withClampedSpring(finalTransform) @@ -348,8 +361,7 @@ const ImageItem = ({ const styles = StyleSheet.create({ container: { - width: SCREEN.width, - height: SCREEN.height, + height: '100%', overflow: 'hidden', }, image: { @@ -367,19 +379,20 @@ const styles = StyleSheet.create({ function getScaledDimensions( imageAspect: number, scale: number, + screenSize: {width: number; height: number}, ): ImageDimensions { 'worklet' - const screenAspect = SCREEN.width / SCREEN.height + const screenAspect = screenSize.width / screenSize.height const isLandscape = imageAspect > screenAspect if (isLandscape) { return { - width: scale * SCREEN.width, - height: (scale * SCREEN.width) / imageAspect, + width: scale * screenSize.width, + height: (scale * screenSize.width) / imageAspect, } } else { return { - width: scale * SCREEN.height * imageAspect, - height: scale * SCREEN.height, + width: scale * screenSize.height * imageAspect, + height: scale * screenSize.height, } } } diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx index a96a1c913..e8f36d520 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx @@ -7,15 +7,18 @@ */ import React, {useState} from 'react' -import {ActivityIndicator, Dimensions, StyleSheet} from 'react-native' +import {ActivityIndicator, StyleSheet, View} from 'react-native' import {Gesture, GestureDetector} from 'react-native-gesture-handler' import Animated, { + AnimatedRef, interpolate, + measure, runOnJS, useAnimatedRef, useAnimatedStyle, useSharedValue, } from 'react-native-reanimated' +import {useSafeAreaFrame} from 'react-native-safe-area-context' import {Image} from 'expo-image' import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' @@ -24,7 +27,6 @@ import {ImageSource} from '../../@types' const SWIPE_CLOSE_OFFSET = 75 const SWIPE_CLOSE_VELOCITY = 1 -const SCREEN = Dimensions.get('screen') const MAX_ORIGINAL_IMAGE_ZOOM = 2 const MIN_DOUBLE_TAP_SCALE = 2 @@ -35,6 +37,7 @@ type Props = { onZoom: (scaled: boolean) => void isScrollViewBeingDragged: boolean showControls: boolean + safeAreaRef: AnimatedRef<View> } const ImageItem = ({ @@ -43,20 +46,24 @@ const ImageItem = ({ onZoom, onRequestClose, showControls, + safeAreaRef, }: Props) => { const scrollViewRef = useAnimatedRef<Animated.ScrollView>() const translationY = useSharedValue(0) const [scaled, setScaled] = useState(false) + const screenSizeDelayedForJSThreadOnly = useSafeAreaFrame() const [imageAspect, imageDimensions] = useImageDimensions({ src: imageSrc.uri, knownDimensions: imageSrc.dimensions, }) const maxZoomScale = imageDimensions - ? (imageDimensions.width / SCREEN.width) * MAX_ORIGINAL_IMAGE_ZOOM + ? (imageDimensions.width / screenSizeDelayedForJSThreadOnly.width) * + MAX_ORIGINAL_IMAGE_ZOOM : 1 const animatedStyle = useAnimatedStyle(() => { return { + flex: 1, opacity: interpolate( translationY.value, [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET], @@ -90,24 +97,13 @@ const ImageItem = ({ setScaled(nextIsScaled) } - function handleDoubleTap(absoluteX: number, absoluteY: number) { + function zoomTo(nextZoomRect: { + x: number + y: number + width: number + height: number + }) { const scrollResponderRef = scrollViewRef?.current?.getScrollResponder() - let nextZoomRect = { - x: 0, - y: 0, - width: SCREEN.width, - height: SCREEN.height, - } - - const willZoom = !scaled - if (willZoom) { - nextZoomRect = getZoomRectAfterDoubleTap( - imageAspect, - absoluteX, - absoluteY, - ) - } - // @ts-ignore scrollResponderRef?.scrollResponderZoomTo({ ...nextZoomRect, // This rect is in screen coordinates @@ -124,8 +120,27 @@ const ImageItem = ({ .numberOfTaps(2) .onEnd(e => { 'worklet' + const screenSize = measure(safeAreaRef) + if (!screenSize) { + return + } const {absoluteX, absoluteY} = e - runOnJS(handleDoubleTap)(absoluteX, absoluteY) + let nextZoomRect = { + x: 0, + y: 0, + width: screenSize.width, + height: screenSize.height, + } + const willZoom = !scaled + if (willZoom) { + nextZoomRect = getZoomRectAfterDoubleTap( + imageAspect, + absoluteX, + absoluteY, + screenSize, + ) + } + runOnJS(zoomTo)(nextZoomRect) }) const composedGesture = Gesture.Exclusive(doubleTap, singleTap) @@ -135,13 +150,13 @@ const ImageItem = ({ <Animated.ScrollView // @ts-ignore Something's up with the types here ref={scrollViewRef} - style={styles.listItem} pinchGestureEnabled showsHorizontalScrollIndicator={false} showsVerticalScrollIndicator={false} maximumZoomScale={maxZoomScale} - onScroll={scrollHandler}> - <Animated.View style={[styles.imageScrollContainer, animatedStyle]}> + onScroll={scrollHandler} + contentContainerStyle={styles.scrollContainer}> + <Animated.View style={animatedStyle}> <ActivityIndicator size="small" color="#FFF" style={styles.loading} /> <Image contentFit="contain" @@ -161,17 +176,6 @@ const ImageItem = ({ } const styles = StyleSheet.create({ - imageScrollContainer: { - height: SCREEN.height, - }, - listItem: { - width: SCREEN.width, - height: SCREEN.height, - }, - image: { - width: SCREEN.width, - height: SCREEN.height, - }, loading: { position: 'absolute', top: 0, @@ -179,30 +183,38 @@ const styles = StyleSheet.create({ right: 0, bottom: 0, }, + scrollContainer: { + flex: 1, + }, + image: { + flex: 1, + }, }) const getZoomRectAfterDoubleTap = ( imageAspect: number | undefined, touchX: number, touchY: number, + screenSize: {width: number; height: number}, ): { x: number y: number width: number height: number } => { + 'worklet' if (!imageAspect) { return { x: 0, y: 0, - width: SCREEN.width, - height: SCREEN.height, + width: screenSize.width, + height: screenSize.height, } } // First, let's figure out how much we want to zoom in. // We want to try to zoom in at least close enough to get rid of black bars. - const screenAspect = SCREEN.width / SCREEN.height + const screenAspect = screenSize.width / screenSize.height const zoom = Math.max( imageAspect / screenAspect, screenAspect / imageAspect, @@ -213,25 +225,25 @@ const getZoomRectAfterDoubleTap = ( // Next, we'll be calculating the rectangle to "zoom into" in screen coordinates. // We already know the zoom level, so this gives us the rectangle size. - let rectWidth = SCREEN.width / zoom - let rectHeight = SCREEN.height / zoom + let rectWidth = screenSize.width / zoom + let rectHeight = screenSize.height / zoom // Before we settle on the zoomed rect, figure out the safe area it has to be inside. // We don't want to introduce new black bars or make existing black bars unbalanced. let minX = 0 let minY = 0 - let maxX = SCREEN.width - rectWidth - let maxY = SCREEN.height - rectHeight + let maxX = screenSize.width - rectWidth + let maxY = screenSize.height - rectHeight if (imageAspect >= screenAspect) { // The image has horizontal black bars. Exclude them from the safe area. - const renderedHeight = SCREEN.width / imageAspect - const horizontalBarHeight = (SCREEN.height - renderedHeight) / 2 + const renderedHeight = screenSize.width / imageAspect + const horizontalBarHeight = (screenSize.height - renderedHeight) / 2 minY += horizontalBarHeight maxY -= horizontalBarHeight } else { // The image has vertical black bars. Exclude them from the safe area. - const renderedWidth = SCREEN.height * imageAspect - const verticalBarWidth = (SCREEN.width - renderedWidth) / 2 + const renderedWidth = screenSize.height * imageAspect + const verticalBarWidth = (screenSize.width - renderedWidth) / 2 minX += verticalBarWidth maxX -= verticalBarWidth } @@ -246,7 +258,7 @@ const getZoomRectAfterDoubleTap = ( rectX = Math.max(rectX, minX) } else { // Keep the rect centered on the screen so that black bars are balanced. - rectX = SCREEN.width / 2 - rectWidth / 2 + rectX = screenSize.width / 2 - rectWidth / 2 } let rectY if (maxY >= minY) { @@ -257,7 +269,7 @@ const getZoomRectAfterDoubleTap = ( rectY = Math.max(rectY, minY) } else { // Keep the rect centered on the screen so that black bars are balanced. - rectY = SCREEN.height / 2 - rectHeight / 2 + rectY = screenSize.height / 2 - rectHeight / 2 } return { diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx index 4cb7903ef..383bec995 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx @@ -2,6 +2,7 @@ import React from 'react' import {View} from 'react-native' +import {AnimatedRef} from 'react-native-reanimated' import {ImageSource} from '../../@types' @@ -12,6 +13,7 @@ type Props = { onZoom: (scaled: boolean) => void isScrollViewBeingDragged: boolean showControls: boolean + safeAreaRef: AnimatedRef<View> } const ImageItem = (_props: Props) => { 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 |