| @@ -48,9 +48,19 @@ export type OnSwipeCompleteParams = { |
| | |
| type State = { | type State = { |
| showContent: boolean; | showContent: boolean; |
| + |
| + |
| + |
| + |
| + isAnimatingContent: boolean; |
| isVisible: boolean; | isVisible: boolean; |
| deviceWidth: number; | deviceWidth: number; |
| deviceHeight: number; | deviceHeight: number; |
| + |
| + |
| + |
| + realWindowWidth: number; |
| + realWindowHeight: number; |
| isSwipeable: boolean; | isSwipeable: boolean; |
| pan: OrNull<Animated.ValueXY>; | pan: OrNull<Animated.ValueXY>; |
| }; | }; |
| @@ -188,9 +198,12 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| | |
| state: State = { | state: State = { |
| showContent: true, | showContent: true, |
| + isAnimatingContent: false, |
| isVisible: false, | isVisible: false, |
| deviceWidth: Dimensions.get('window').width, | deviceWidth: Dimensions.get('window').width, |
| deviceHeight: Dimensions.get('window').height, | deviceHeight: Dimensions.get('window').height, |
| + realWindowWidth: Dimensions.get('window').width, |
| + realWindowHeight: Dimensions.get('window').height, |
| isSwipeable: !!this.props.swipeDirection, | isSwipeable: !!this.props.swipeDirection, |
| pan: null, | pan: null, |
| }; | }; |
| @@ -198,9 +211,15 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| isTransitioning = false; | isTransitioning = false; |
| inSwipeClosingState = false; | inSwipeClosingState = false; |
| currentSwipingDirection: OrNull<Direction> = null; | currentSwipingDirection: OrNull<Direction> = null; |
| + |
| + |
| + |
| + |
| + isUnmounted = false; |
| | |
| animationIn: string; | animationIn: string; |
| animationOut: string; | animationOut: string; |
| + backdropAnimated: Animated.Value; |
| backdropRef: any; | backdropRef: any; |
| contentRef: any; | contentRef: any; |
| panResponder: OrNull<PanResponderInstance> = null; | panResponder: OrNull<PanResponderInstance> = null; |
| @@ -217,6 +236,7 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| | |
| this.animationIn = animationIn; | this.animationIn = animationIn; |
| this.animationOut = animationOut; | this.animationOut = animationOut; |
| + this.backdropAnimated = new Animated.Value(0); |
| | |
| if (this.state.isSwipeable) { | if (this.state.isSwipeable) { |
| this.state = { | this.state = { |
| @@ -230,13 +250,17 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| ...this.state, | ...this.state, |
| isVisible: true, | isVisible: true, |
| showContent: true, | showContent: true, |
| + isAnimatingContent: true, |
| }; | }; |
| } | } |
| } | } |
| | |
| static getDerivedStateFromProps(nextProps: Readonly<ModalProps>, state: State) { | static getDerivedStateFromProps(nextProps: Readonly<ModalProps>, state: State) { |
| if (!state.isVisible && nextProps.isVisible) { | if (!state.isVisible && nextProps.isVisible) { |
| - return { isVisible: true, showContent: true }; | + // isAnimatingContent is raised here rather than in open() so that the |
| + |
| + |
| + return {isVisible: true, showContent: true, isAnimatingContent: true}; |
| } | } |
| return null; | return null; |
| } | } |
| @@ -267,6 +291,7 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| }; | }; |
| | |
| componentWillUnmount() { | componentWillUnmount() { |
| + this.isUnmounted = true; |
| if (this.isVersionGreaterOrEqual(0, 77, 0)) { | if (this.isVersionGreaterOrEqual(0, 77, 0)) { |
| if (this.backListener) { | if (this.backListener) { |
| this.backListener.remove(); | this.backListener.remove(); |
| @@ -284,6 +309,13 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| InteractionManager.clearInteractionHandle(this.interactionHandle); | InteractionManager.clearInteractionHandle(this.interactionHandle); |
| this.interactionHandle = null; | this.interactionHandle = null; |
| } | } |
| + |
| + if (this.backdropAnimated && this.backdropAnimated.stopAnimation) { |
| + this.backdropAnimated.stopAnimation(); |
| + } |
| + |
| + this.contentRef = null; |
| + this.backdropRef = null; |
| } | } |
| | |
| componentDidUpdate(prevProps: ModalProps) { | componentDidUpdate(prevProps: ModalProps) { |
| @@ -299,15 +331,13 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| this.animationIn = animationIn; | this.animationIn = animationIn; |
| this.animationOut = animationOut; | this.animationOut = animationOut; |
| } | } |
| - // If backdrop opacity has been changed then make sure to update it | + // If backdrop opacity has been changed then animate the Animated.Value |
| - if ( | + if (this.props.backdropOpacity !== prevProps.backdropOpacity) { |
| - this.props.backdropOpacity !== prevProps.backdropOpacity && | + Animated.timing(this.backdropAnimated, { |
| - this.backdropRef | + toValue: this.props.backdropOpacity, |
| - ) { | + duration: this.props.backdropTransitionInTiming, |
| - this.backdropRef.transitionTo( | + useNativeDriver: this.props.useNativeDriverForBackdrop === true, |
| - { opacity: this.props.backdropOpacity }, | + }).start(); |
| - this.props.backdropTransitionInTiming, | |
| - ); | |
| } | } |
| | |
| if (this.props.isVisible && !prevProps.isVisible) { | if (this.props.isVisible && !prevProps.isVisible) { |
| @@ -319,6 +349,19 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| } | } |
| getDeviceHeight = () => this.props.deviceHeight || this.state.deviceHeight; | getDeviceHeight = () => this.props.deviceHeight || this.state.deviceHeight; |
| getDeviceWidth = () => this.props.deviceWidth || this.state.deviceWidth; | getDeviceWidth = () => this.props.deviceWidth || this.state.deviceWidth; |
| + |
| + |
| + |
| + |
| + |
| + |
| + |
| + |
| + |
| + getGestureDeviceHeight = () => |
| + this.props.deviceHeight || Dimensions.get('window').height; |
| + getGestureDeviceWidth = () => |
| + this.props.deviceWidth || Dimensions.get('window').width; |
| onBackButtonPress = () => { | onBackButtonPress = () => { |
| if (this.props.onBackButtonPress && this.props.isVisible) { | if (this.props.onBackButtonPress && this.props.isVisible) { |
| this.props.onBackButtonPress(); | this.props.onBackButtonPress(); |
| @@ -379,13 +422,16 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| ) { | ) { |
| return false; | return false; |
| } | } |
| - if (this.props.onSwipeStart) { | |
| - this.props.onSwipeStart(gestureState); | |
| - } | |
| | |
| | |
| | |
| this.currentSwipingDirection = null; | this.currentSwipingDirection = null; |
| + |
| + |
| + |
| + |
| + |
| + |
| return true; | return true; |
| }, | }, |
| onPanResponderMove: (evt, gestureState) => { | onPanResponderMove: (evt, gestureState) => { |
| @@ -395,6 +441,19 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| if (gestureState.dx === 0 && gestureState.dy === 0) { | if (gestureState.dx === 0 && gestureState.dy === 0) { |
| return; | return; |
| } | } |
| + |
| + |
| + |
| + |
| + if ( |
| + Math.abs(gestureState.dx) < this.props.panResponderThreshold && |
| + Math.abs(gestureState.dy) < this.props.panResponderThreshold |
| + ) { |
| + return; |
| + } |
| + if (this.props.onSwipeStart) { |
| + this.props.onSwipeStart(gestureState); |
| + } |
| | |
| this.currentSwipingDirection = this.getSwipingDirection(gestureState); | this.currentSwipingDirection = this.getSwipingDirection(gestureState); |
| animEvt = this.createAnimationEventForSwipe(); | animEvt = this.createAnimationEventForSwipe(); |
| @@ -405,10 +464,14 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| const newOpacityFactor = | const newOpacityFactor = |
| 1 - this.calcDistancePercentage(gestureState); | 1 - this.calcDistancePercentage(gestureState); |
| | |
| - this.backdropRef && | + // Update backdrop opacity immediately during swipe. setValue() is |
| - this.backdropRef.transitionTo({ | + // synchronous and works regardless of useNativeDriver setting — |
| - opacity: this.props.backdropOpacity * newOpacityFactor, | + // it directly sets the Animated.Value without scheduling an |
| - }); | + // animation (transitionTo would start a new spring per move event |
| + |
| + this.backdropAnimated.setValue( |
| + this.props.backdropOpacity * newOpacityFactor, |
| + ); |
| | |
| animEvt!(evt, gestureState); | animEvt!(evt, gestureState); |
| | |
| @@ -461,14 +524,20 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| } | } |
| | |
| | |
| - if (this.props.onSwipeCancel) { | + // onSwipeCancel pairs with onSwipeStart, which fires only after the |
| + |
| + |
| + if (this.currentSwipingDirection && this.props.onSwipeCancel) { |
| this.props.onSwipeCancel(gestureState); | this.props.onSwipeCancel(gestureState); |
| } | } |
| | |
| if (this.backdropRef) { | if (this.backdropRef) { |
| - this.backdropRef.transitionTo({ | + // Reset backdrop opacity with a smooth animation |
| - opacity: this.props.backdropOpacity, | + Animated.timing(this.backdropAnimated, { |
| - }); | + toValue: this.props.backdropOpacity, |
| + duration: this.props.backdropTransitionInTiming, |
| + useNativeDriver: this.props.useNativeDriverForBackdrop === true, |
| + }).start(); |
| } | } |
| | |
| Animated.spring(this.state.pan!, { | Animated.spring(this.state.pan!, { |
| @@ -517,8 +586,7 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| case 'down': | case 'down': |
| return ( | return ( |
| (gestureState.moveY - gestureState.y0) / | (gestureState.moveY - gestureState.y0) / |
| - ((this.props.deviceHeight || this.state.deviceHeight) - | + (this.getGestureDeviceHeight() - gestureState.y0) |
| - gestureState.y0) | |
| ); | ); |
| case 'up': | case 'up': |
| return reversePercentage(gestureState.moveY / gestureState.y0); | return reversePercentage(gestureState.moveY / gestureState.y0); |
| @@ -527,7 +595,7 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| case 'right': | case 'right': |
| return ( | return ( |
| (gestureState.moveX - gestureState.x0) / | (gestureState.moveX - gestureState.x0) / |
| - ((this.props.deviceWidth || this.state.deviceWidth) - gestureState.x0) | + (this.getGestureDeviceWidth() - gestureState.x0) |
| ); | ); |
| | |
| default: | default: |
| @@ -590,31 +658,141 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| return false; | return false; |
| }; | }; |
| | |
| + |
| + |
| + |
| + |
| + |
| + |
| + constrainToSupportedOrientation = (width: number, height: number) => { |
| + |
| + |
| + |
| + if (!['harmony'].includes(Platform.OS)) { |
| + return {width, height}; |
| + } |
| + const supported = this.props.supportedOrientations; |
| + if (!supported || supported.length === 0) { |
| + return {width, height}; |
| + } |
| + const orientation = width > height ? 'landscape' : 'portrait'; |
| + const isSupported = |
| + supported.includes(orientation as Orientation) || |
| + (orientation === 'portrait' && |
| + supported.includes('portrait-upside-down')) || |
| + (orientation === 'landscape' && |
| + (supported.includes('landscape-left') || |
| + supported.includes('landscape-right'))); |
| + if (isSupported) { |
| + return {width, height}; |
| + } |
| + |
| + return {width: height, height: width}; |
| + }; |
| + |
| + constrainDimensionsToSupportedOrientation = () => { |
| + if (this.props.deviceHeight || this.props.deviceWidth) { |
| + return; |
| + } |
| + const windowDimensions = Dimensions.get('window'); |
| + const {width, height} = this.constrainToSupportedOrientation( |
| + windowDimensions.width, |
| + windowDimensions.height, |
| + ); |
| + if ( |
| + width !== this.state.deviceWidth || |
| + height !== this.state.deviceHeight |
| + ) { |
| + this.setState({deviceWidth: width, deviceHeight: height}); |
| + } |
| + }; |
| + |
| handleDimensionsUpdate = () => { | handleDimensionsUpdate = () => { |
| if (!this.props.deviceHeight && !this.props.deviceWidth) { | if (!this.props.deviceHeight && !this.props.deviceWidth) { |
| | |
| | |
| - const deviceWidth = Dimensions.get('window').width; | + const windowDimensions = Dimensions.get('window'); |
| - const deviceHeight = Dimensions.get('window').height; | + const {width, height} = this.constrainToSupportedOrientation( |
| + windowDimensions.width, |
| + windowDimensions.height, |
| + ); |
| + const deviceWidth = width; |
| + const deviceHeight = height; |
| if ( | if ( |
| deviceWidth !== this.state.deviceWidth || | deviceWidth !== this.state.deviceWidth || |
| - deviceHeight !== this.state.deviceHeight | + deviceHeight !== this.state.deviceHeight || |
| + |
| + |
| + |
| + windowDimensions.width !== this.state.realWindowWidth || |
| + windowDimensions.height !== this.state.realWindowHeight |
| ) { | ) { |
| - this.setState({ deviceWidth, deviceHeight }); | + this.setState({ |
| + deviceWidth, |
| + deviceHeight, |
| + realWindowWidth: windowDimensions.width, |
| + realWindowHeight: windowDimensions.height, |
| + }); |
| } | } |
| } | } |
| }; | }; |
| | |
| + |
| + |
| + |
| + |
| + setAnimatingContent = (isAnimatingContent: boolean) => { |
| + if ( |
| + this.isUnmounted || |
| + this.state.isAnimatingContent === isAnimatingContent |
| + ) { |
| + return; |
| + } |
| + this.setState({isAnimatingContent}); |
| + }; |
| + |
| + |
| + |
| + |
| + |
| + |
| + completeOpenWithoutAnimation = () => { |
| + this.isTransitioning = false; |
| + if (this.interactionHandle) { |
| + InteractionManager.clearInteractionHandle(this.interactionHandle); |
| + this.interactionHandle = null; |
| + } |
| + if (this.isUnmounted) { |
| + return; |
| + } |
| + if (!this.props.isVisible) { |
| + this.close(); |
| + } else { |
| + this.setAnimatingContent(false); |
| + this.props.onModalShow(); |
| + } |
| + }; |
| + |
| open = () => { | open = () => { |
| if (this.isTransitioning) { | if (this.isTransitioning) { |
| return; | return; |
| } | } |
| this.isTransitioning = true; | this.isTransitioning = true; |
| + this.setAnimatingContent(true); |
| + |
| + |
| + |
| + this.constrainDimensionsToSupportedOrientation(); |
| if (this.backdropRef) { | if (this.backdropRef) { |
| - this.backdropRef.transitionTo( | + // Rewind the backdrop to fully transparent before fading in, so the |
| - { opacity: this.props.backdropOpacity }, | + // fade-in always starts from a known value. |
| - this.props.backdropTransitionInTiming, | + this.backdropAnimated.stopAnimation(); |
| - ); | + this.backdropAnimated.setValue(0); |
| + Animated.timing(this.backdropAnimated, { |
| + toValue: this.props.backdropOpacity, |
| + duration: this.props.backdropTransitionInTiming, |
| + useNativeDriver: this.props.useNativeDriverForBackdrop === true, |
| + }).start(); |
| } | } |
| | |
| | |
| @@ -632,17 +810,16 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| this.contentRef | this.contentRef |
| .animate(this.animationIn, this.props.animationInTiming) | .animate(this.animationIn, this.props.animationInTiming) |
| .then(() => { | .then(() => { |
| - this.isTransitioning = false; | + if (this.isUnmounted) { |
| - if (this.interactionHandle) { | + return; |
| - InteractionManager.clearInteractionHandle(this.interactionHandle); | |
| - this.interactionHandle = null; | |
| - } | |
| - if (!this.props.isVisible) { | |
| - this.close(); | |
| - } else { | |
| - this.props.onModalShow(); | |
| } | } |
| + this.completeOpenWithoutAnimation(); |
| }); | }); |
| + } else { |
| + |
| + |
| + |
| + this.completeOpenWithoutAnimation(); |
| } | } |
| }; | }; |
| | |
| @@ -651,11 +828,16 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| return; | return; |
| } | } |
| this.isTransitioning = true; | this.isTransitioning = true; |
| + this.setAnimatingContent(true); |
| if (this.backdropRef) { | if (this.backdropRef) { |
| - this.backdropRef.transitionTo( | + if (this.backdropAnimated && this.backdropAnimated.stopAnimation) { |
| - { opacity: 0 }, | + this.backdropAnimated.stopAnimation(); |
| - this.props.backdropTransitionOutTiming, | + } |
| - ); | + Animated.timing(this.backdropAnimated, { |
| + toValue: 0, |
| + duration: this.props.backdropTransitionOutTiming, |
| + useNativeDriver: this.props.useNativeDriverForBackdrop === true, |
| + }).start(); |
| } | } |
| | |
| let animationOut = this.animationOut; | let animationOut = this.animationOut; |
| @@ -681,31 +863,53 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| this.contentRef | this.contentRef |
| .animate(animationOut, this.props.animationOutTiming) | .animate(animationOut, this.props.animationOutTiming) |
| .then(() => { | .then(() => { |
| - this.isTransitioning = false; | + if (this.isUnmounted) { |
| - if (this.interactionHandle) { | + return; |
| - InteractionManager.clearInteractionHandle(this.interactionHandle); | |
| - this.interactionHandle = null; | |
| - } | |
| - if (this.props.isVisible) { | |
| - this.open(); | |
| - } else { | |
| - this.setState( | |
| - { | |
| - showContent: false, | |
| - }, | |
| - () => { | |
| - this.setState( | |
| - { | |
| - isVisible: false, | |
| - }, | |
| - () => { | |
| - this.props.onModalHide(); | |
| - }, | |
| - ); | |
| - }, | |
| - ); | |
| } | } |
| + this.finalizeClose(); |
| }); | }); |
| + } else { |
| + |
| + |
| + |
| + this.props.onModalWillHide && this.props.onModalWillHide(); |
| + this.finalizeClose(); |
| + } |
| + }; |
| + |
| + |
| + |
| + finalizeClose = () => { |
| + if (this.isUnmounted) { |
| + return; |
| + } |
| + this.isTransitioning = false; |
| + if (this.interactionHandle) { |
| + InteractionManager.clearInteractionHandle(this.interactionHandle); |
| + this.interactionHandle = null; |
| + } |
| + if (this.props.isVisible) { |
| + this.open(); |
| + } else { |
| + this.setState( |
| + { |
| + showContent: false, |
| + }, |
| + () => { |
| + |
| + |
| + |
| + this.setState( |
| + { |
| + isVisible: false, |
| + isAnimatingContent: false, |
| + }, |
| + () => { |
| + this.props.onModalHide(); |
| + }, |
| + ); |
| + }, |
| + ); |
| } | } |
| }; | }; |
| makeBackdrop = () => { | makeBackdrop = () => { |
| @@ -723,16 +927,20 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| const { | const { |
| customBackdrop, | customBackdrop, |
| backdropColor, | backdropColor, |
| - useNativeDriver, | |
| - useNativeDriverForBackdrop, | |
| onBackdropPress, | onBackdropPress, |
| } = this.props; | } = this.props; |
| const hasCustomBackdrop = !!this.props.customBackdrop; | const hasCustomBackdrop = !!this.props.customBackdrop; |
| | |
| const backdropComputedStyle = [ | const backdropComputedStyle = [ |
| { | { |
| - width: this.getDeviceWidth(), | + // The backdrop must cover the real window: getDeviceWidth()/ |
| - height: this.getDeviceHeight(), | + // getDeviceHeight() may return the orientation-constrained |
| + |
| + |
| + |
| + |
| + width: this.getGestureDeviceWidth(), |
| + height: this.getGestureDeviceHeight(), |
| backgroundColor: | backgroundColor: |
| this.state.showContent && !hasCustomBackdrop | this.state.showContent && !hasCustomBackdrop |
| ? backdropColor | ? backdropColor |
| @@ -741,16 +949,15 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| ]; | ]; |
| | |
| const backdropWrapper = ( | const backdropWrapper = ( |
| - <animatable.View | + <Animated.View |
| - ref={ref => (this.backdropRef = ref)} | + ref={(ref: any) => (this.backdropRef = ref)} |
| - useNativeDriver={ | + style={[ |
| - useNativeDriverForBackdrop !== undefined | + styles.backdrop, |
| - ? useNativeDriverForBackdrop | + backdropComputedStyle, |
| - : useNativeDriver | + {opacity: this.backdropAnimated}, |
| - } | + ]}> |
| - style={[styles.backdrop, backdropComputedStyle]}> | |
| {hasCustomBackdrop && customBackdrop} | {hasCustomBackdrop && customBackdrop} |
| - </animatable.View> | + </Animated.View> |
| ); | ); |
| | |
| if (hasCustomBackdrop) { | if (hasCustomBackdrop) { |
| @@ -803,11 +1010,13 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| panHandlers = { ...this.panResponder!.panHandlers }; | panHandlers = { ...this.panResponder!.panHandlers }; |
| | |
| if (useNativeDriver) { | if (useNativeDriver) { |
| - const translateY=this.state.pan.y._value | + // getTranslateTransform() keeps the Animated.ValueXY nodes in the |
| - const translateX=this.state.pan.x._value | + // transform so the (JS-driven) pan updates move the content live. |
| - | + // Reading pan._value here would freeze the transform at the values |
| + |
| + |
| panPosition = { | panPosition = { |
| - transform: [{"translateX":Number(translateX)},{"translateY":Number(translateY)}], | + transform: this.state.pan!.getTranslateTransform(), |
| }; | }; |
| } else { | } else { |
| panPosition = this.state.pan!.getLayout(); | panPosition = this.state.pan!.getLayout(); |
| @@ -815,11 +1024,13 @@ export class ReactNativeModal extends React.Component<ModalProps, State> { |
| } | } |
| | |
| | |
| - // to enhance performance. | + // to enhance performance. Upstream gated this on useNativeDriver and on |
| + |
| + |
| + |
| const _children = | const _children = |
| this.props.hideModalContentWhileAnimating && | this.props.hideModalContentWhileAnimating && |
| - this.props.useNativeDriver && | + this.state.isAnimatingContent ? ( |
| - !this.state.showContent ? ( | |
| <animatable.View /> | <animatable.View /> |
| ) : ( | ) : ( |
| children | children |
| |
【AI-Review】【一般】【基础代码问题】【代码逻辑错误】orientation约束导致backdrop无法覆盖全屏
● 问题:
constrainToSupportedOrientation在方向不支持时交换宽高(line 659:return {width: height, height: width}),交换后的尺寸经constrainDimensionsToSupportedOrientation(line 675)和handleDimensionsUpdate(line 694)写入state.deviceWidth/deviceHeight。而makeBackdrop(line 867-868)使用getDeviceWidth()/getDeviceHeight()设置 backdrop 的宽高,返回的正是约束后的 state 值。styles.backdrop虽设置了top:0, bottom:0, left:0, right:0,但backdropComputedStyle中的width/height覆盖了拉伸行为,backdrop 实际尺寸为交换后的值。触发路径:用户设置
supportedOrientations={['portrait']}(仅竖屏)→ 设备处于横屏(如 800×400)→open()调用constrainDimensionsToSupportedOrientation()→constrainToSupportedOrientation(800, 400)判定 orientation='landscape',isSupported=false → 返回{width:400, height:800}(交换)→ state 更新为 deviceWidth=400, deviceHeight=800 →makeBackdrop渲染 backdrop 为 400×800 → 在 800×400 屏幕上仅覆盖左上 400×400,右侧 400 像素未被覆盖。● 影响:在 HarmonyOS 上,当
supportedOrientations不包含当前设备方向时,backdrop 无法覆盖全屏,用户可以看到并操作 backdrop 外的区域(Modal 为transparent={true}),破坏模态框遮挡背景的设计意图。● 建议:backdrop 应使用实际屏幕尺寸(
Dimensions.get('window'))而非约束后的 state 值,确保全屏覆盖。可将makeBackdrop中的width/height改为直接使用Dimensions.get('window').width/height,或将约束后尺寸仅用于内容布局,不影响 backdrop 全屏覆盖。