cbbfcd / react-native-lightbox

Images etc in Full Screen Lightbox Popovers for React Native
MIT License
63 stars 27 forks source link

Fixed the lightbox not resizing properly on orientation #17

Closed J-Mann-123 closed 1 month ago

J-Mann-123 commented 9 months ago

I found a solution to the lightbox not resizing correctly when the orientation is changed. It uses 2 new values named deviceWidth and deviceHeight which are wrapped in a useState hook. Also I added a useEffect hook which listens to event changes. Then in openStyle, in width and height I changed the outputRange to use deviceWidth and deviceHeight.

This was an open ticket in the original react-native-lightbox repo: Ticket Link

import React, { useRef, useEffect, useState } from "react";
import { Animated, Dimensions, PanResponder, Platform, StyleSheet, StatusBar, TouchableOpacity, Text, Modal, } from "react-native";
import { useGesture, useNextTick } from "./hooks";
const { width: WINDOW_WIDTH, height: WINDOW_HEIGHT } = Dimensions.get("window");
const isIOS = Platform.OS === "ios";
const getDefaultTarget = () => ({ x: 0, y: 0, opacity: 1 });
const styles = StyleSheet.create({
    background: {
        position: "absolute",
        top: 0,
        left: 0,
        width: WINDOW_WIDTH,
        height: WINDOW_HEIGHT,
    },
    open: {
        position: "absolute",
        flex: 1,
        justifyContent: "center",
        // Android pan handlers crash without this declaration:
        backgroundColor: "transparent",
    },
    header: {
        position: "absolute",
        top: 0,
        left: 0,
        width: WINDOW_WIDTH,
        backgroundColor: "transparent",
    },
    closeButton: {
        fontSize: 35,
        color: "white",
        lineHeight: 60,
        width: 70,
        textAlign: "center",
        shadowOffset: {
            width: 0,
            height: 0,
        },
        shadowRadius: 1.5,
        shadowColor: "black",
        shadowOpacity: 0.8,
    },
});
const LightboxOverlay = ({ useNativeDriver, dragDismissThreshold, springConfig, isOpen, onClose, willClose, didOpen, swipeToDismiss, origin, backgroundColor, renderHeader, modalProps, children, doubleTapZoomEnabled, doubleTapGapTimer, doubleTapCallback, doubleTapZoomToCenter, doubleTapMaxZoom, doubleTapZoomStep, doubleTapInitialScale, doubleTapAnimationDuration, longPressGapTimer, longPressCallback }) => {
    const _panResponder = useRef();
    const pan = useRef(new Animated.Value(0));
    const openVal = useRef(new Animated.Value(0));
    const handlers = useRef();
    const [gesture, animations] = useGesture({
        useNativeDriver,
        doubleTapZoomEnabled,
        doubleTapGapTimer,
        doubleTapCallback,
        doubleTapZoomToCenter,
        doubleTapMaxZoom,
        doubleTapZoomStep,
        doubleTapInitialScale,
        doubleTapAnimationDuration,
        longPressGapTimer,
        longPressCallback
    });
    const [deviceWidth, setDeviceWidth] = useState(Dimensions.get('window').width);
    const [deviceHeight, setDeviceHeight] = useState(Dimensions.get('window').height);
    const [{ isAnimating, isPanning, target }, setState] = useState({
        isAnimating: false,
        isPanning: false,
        target: getDefaultTarget(),
    });
    const handleCloseNextTick = useNextTick(onClose);
    const close = () => {
        willClose();
        if (isIOS) {
            StatusBar.setHidden(false, "fade");
        }
        gesture.reset();
        setState((s) => ({
            ...s,
            isAnimating: true,
        }));
        Animated.spring(openVal.current, {
            toValue: 0,
            ...springConfig,
            useNativeDriver,
        }).start(({ finished }) => {
            if (finished) {
                setState((s) => ({ ...s, isAnimating: false }));
                handleCloseNextTick();
            }
        });
    };
    const open = () => {
        if (isIOS) {
            StatusBar.setHidden(true, "fade");
        }
        pan.current.setValue(0);
        setState((s) => ({
            ...s,
            isAnimating: true,
            target: getDefaultTarget(),
        }));
        Animated.spring(openVal.current, {
            toValue: 1,
            ...springConfig,
            useNativeDriver,
        }).start(({ finished }) => {
            if (finished) {
                setState((s) => ({ ...s, isAnimating: false }));
                didOpen();
            }
        });
    };
    const initPanResponder = () => {
        _panResponder.current = PanResponder.create({
            // Ask to be the responder:
            onStartShouldSetPanResponder: () => !isAnimating,
            onStartShouldSetPanResponderCapture: () => !isAnimating,
            onMoveShouldSetPanResponder: () => !isAnimating,
            onMoveShouldSetPanResponderCapture: () => !isAnimating,
            onPanResponderGrant: (e, gestureState) => {
                gesture.init();
                pan.current.setValue(0);
                setState((s) => ({ ...s, isPanning: true }));
                gesture.onLongPress(e, gestureState);
                gesture.onDoubleTap(e, gestureState);
            },
            onPanResponderMove: Animated.event([null, { dy: pan.current }], {
                useNativeDriver,
            }),
            onPanResponderTerminationRequest: () => true,
            onPanResponderRelease: (evt, gestureState) => {
                gesture.release();
                if (gesture.isDoubleTaped)
                    return;
                if (gesture.isLongPressed)
                    return;
                if (Math.abs(gestureState.dy) > dragDismissThreshold) {
                    setState((s) => ({
                        ...s,
                        isPanning: false,
                        target: {
                            y: gestureState.dy,
                            x: gestureState.dx,
                            opacity: 1 - Math.abs(gestureState.dy / WINDOW_HEIGHT),
                        },
                    }));
                    close();
                }
                else {
                    Animated.spring(pan.current, {
                        toValue: 0,
                        ...springConfig,
                        useNativeDriver,
                    }).start(({ finished }) => {
                        finished && setState((s) => ({ ...s, isPanning: false }));
                    });
                }
            },
        });
    };
    useEffect(() => {
        initPanResponder();
    }, [useNativeDriver, isAnimating]);
    useEffect(() => {
        const onChange = ({ window }) => {
            setDeviceWidth(window.width);
            setDeviceHeight(window.height);
        };
        const removeEventListener = () => {
            if (Dimensions.removeEventListener) {
                Dimensions.removeEventListener('change', onChange);
            }
        };
        Dimensions.addEventListener('change', onChange);
        return removeEventListener;
    }, []);
    useEffect(() => {
        isOpen && open();
    }, [isOpen]);
    useEffect(() => {
        if (_panResponder.current && swipeToDismiss) {
            handlers.current = _panResponder.current.panHandlers;
        }
    }, [swipeToDismiss, _panResponder.current]);
    const lightboxOpacityStyle = {
        opacity: openVal.current.interpolate({
            inputRange: [0, 1],
            outputRange: [0, target.opacity],
        }),
    };
    let dragStyle;
    if (isPanning) {
        dragStyle = {
            top: pan.current,
        };
        lightboxOpacityStyle.opacity = pan.current.interpolate({
            inputRange: [-WINDOW_HEIGHT, 0, WINDOW_HEIGHT],
            outputRange: [0, 1, 0],
        });
    }
    const openStyle = [
        styles.open,
        {
            left: openVal.current.interpolate({
                inputRange: [0, 1],
                outputRange: [origin.x, target.x],
            }),
            top: openVal.current.interpolate({
                inputRange: [0, 1],
                outputRange: [origin.y, target.y],
            }),
            width: openVal.current.interpolate({
                inputRange: [0, 1],
                outputRange: [origin.width, deviceWidth],
            }),
            height: openVal.current.interpolate({
                inputRange: [0, 1],
                outputRange: [origin.height, deviceHeight],
            }),
        },
    ];

    const background = (<Animated.View style={[styles.background, {
        backgroundColor,
        width: deviceWidth,
        height: deviceHeight,
    }, lightboxOpacityStyle]}></Animated.View>);
    const header = (<Animated.View style={[styles.header, lightboxOpacityStyle]}>
        {renderHeader ? (renderHeader(close)) : (<TouchableOpacity onPress={close}>
            <Text style={styles.closeButton}>×</Text>
        </TouchableOpacity>)}
    </Animated.View>);
    const content = (<Animated.View style={[openStyle, dragStyle, animations]} {...handlers.current}>
        {children}
    </Animated.View>);
    return (<Modal visible={isOpen} transparent={true} onRequestClose={close} {...modalProps}>
        {background}
        {content}
        {header}
    </Modal>);
};
export default LightboxOverlay;

useEffect hook for listening for changes in window.width and window.height

 useEffect(() => {
        const onChange = ({ window }) => {
            setDeviceWidth(window.width);
            setDeviceHeight(window.height);
        };
        const removeEventListener = () => {
            if (Dimensions.removeEventListener) {
                Dimensions.removeEventListener('change', onChange);
            }
        };
        Dimensions.addEventListener('change', onChange);
        return removeEventListener;
    }, []);

useState hooks for deviceWidth and deviceHeight

const [deviceWidth, setDeviceWidth] = useState(Dimensions.get('window').width);
const [deviceHeight, setDeviceHeight] = useState(Dimensions.get('window').height);

Then in openStyle, in width and height I changed the outputRange to use deviceWidth and deviceHeight

const openStyle = [
        styles.open,
        {
            left: openVal.current.interpolate({
                inputRange: [0, 1],
                outputRange: [origin.x, target.x],
            }),
            top: openVal.current.interpolate({
                inputRange: [0, 1],
                outputRange: [origin.y, target.y],
            }),
            width: openVal.current.interpolate({
                inputRange: [0, 1],
                outputRange: [origin.width, deviceWidth],
            }),
            height: openVal.current.interpolate({
                inputRange: [0, 1],
                outputRange: [origin.height, deviceHeight],
            }),
        },
    ];
cbbfcd commented 1 month ago

LGTM, thx~