当前Pull Request已关闭, 关闭人@mazheng
感谢提交 Pull Requests !此PR未通过DCO校验。
校验失败可能原因:
1. 未签署“DCO协议”(开发者原创声明协议),在线签署、查看签署状态。
2. Commits 中未包含 Signed-off-by信息,参考FAQ处理。
修复上述问题后,在PR的评论框输入“check dco” ,单击”评论”,系统将再次进行DCO校验。
当前检测到如下Commits 未包含Signed-off-by信息:
Thanks for submitting a pull request. This pull request has not passed the DCO check.
Possible causes:
1. You have not signed the Developer Certificate of Origin (DCO). Sign the DCO and check DCO status.
2. The commits do not contain the Signed-off-by information. To resolve this issue, see FAQs.
After resolving the preceding issues, enter check dco in the comment box of this pull request and click Comment. The system will check DCO status again.
The following commits do not contain the Signed-off-by information:


【AI-Review】【一般】【基础代码问题】【代码逻辑错误】open() 降级路径跳过 onModalWillShow 回调
● 问题: open() 的 else 分支(line 752-757,contentRef 为 null 时的降级路径)直接调用 this.completeOpenWithoutAnimation()(line 756),未先调用 this.props.onModalWillShow。而同一函数的 if (this.contentRef) 分支在 line 740 明确调用了 this.props.onModalWillShow && this.props.onModalWillShow()。completeOpenWithoutAnimation(line 700-714)在 isVisible 为 true 时调用的是 this.props.onModalShow()(line 712,"已展示"回调),而非 onModalWillShow("将展示"回调)。对比同文件中 close() 函数的 else 分支(line 800-805),在调用 this.finalizeClose() 前于 line 804 调用了 this.props.onModalWillHide && this.props.onModalWillHide(),两个回调路径是对称的——close() 降级路径调用 onModalWillHide,open() 降级路径却未调用 onModalWillShow。
● 影响: 当 contentRef 为 null 但组件尚未 unmount 时(如 PR 注释 line 753-755 所述"the Modal host dismissed the content"场景,RNOH Modal host 在原生层移除内容视图导致 ref 回调被以 null 调用,或组件在 unmount 过程中 ref 被提前置 null 但 isUnmounted 尚未生效的时序窗口),onModalShow 会通过 completeOpenWithoutAnimation 触发,而 onModalWillShow 被跳过。依赖 onModalWillShow → onModalShow 回调时序契约的用户代码(如在 onModalWillShow 中初始化展示前状态、埋点、动画前置准备)将丢失前置回调,而"已展示"回调仍会触发,造成回调时序契约破坏。
● 建议: 在 else 分支的 this.completeOpenWithoutAnimation() 之前增加 this.props.onModalWillShow && this.props.onModalWillShow();,与 close() 的 else 分支(line 804)保持对称。修改后 else 分支应为:
} else {
this.props.onModalWillShow && this.props.onModalWillShow();
this.completeOpenWithoutAnimation();
}


感谢提交 Pull Requests!
Thanks for submitting a pull request.


感谢提交 Pull Requests!
Thanks for submitting a pull request.


【AI-Review】【一般】【基础代码问题】【代码逻辑错误】onPanResponderRelease 启动 Animated.timing 前未停止 in-flight 动画
● 问题:src/modal.tsx:548-555 的 onPanResponderRelease 中,启动 Animated.timing(this.backdropAnimated, ...) 前未调用 this.backdropAnimated.stopAnimation()。而 open()(line 809)、close()(line 857-859)、componentDidUpdate(line 349)在启动相同 backdropAnimated 的 Animated.timing 前都显式调用了 stopAnimation(),此处遗漏了相同处理。
● 影响:当 modal 处于 open/close 的 backdrop 淡入淡出动画期间(默认 backdropTransitionInTiming/OutTiming = 300ms),用户点击内容区域并释放(未达到 swipeThreshold 的轻触或未完成滑动的释放),onPanResponderRelease 会启动第二个 Animated.timing 实例。两个 Animated.timing 同时运行在同一个 Animated.Value 上,每帧都调用 setValue,造成 backdrop 透明度抖动。尤其是 close 动画期间(目标值 0)与 release 重置动画(目标值 backdropOpacity=0.7)目标值相反,冲突更明显,backdrop 透明度会在 0 与 0.7 之间反复跳动。
触发路径:open()/close() 启动 backdrop Animated.timing(300ms)→ 300ms 内用户点击内容区域 → onStartShouldSetPanResponder 返回 true(line 449)→ 用户释放 → onPanResponderRelease 启动第二个 Animated.timing(未 stopAnimation)→ 两个动画并发在同一 Animated.Value 上 → backdrop 透明度抖动。
● 建议:在 if (this.backdropRef) 块内、Animated.timing 之前添加 this.backdropAnimated.stopAnimation(),与 open()/close()/componentDidUpdate 保持一致:
if (this.backdropRef) {
this.backdropAnimated.stopAnimation();
Animated.timing(this.backdropAnimated, {
toValue: this.props.backdropOpacity,
duration: this.props.backdropTransitionInTiming,
useNativeDriver: this.props.useNativeDriverForBackdrop === true,
}).start();
}


【AI-Review】【一般】【基础代码问题】【代码逻辑错误】onPanResponderMove 的 setValue 会被运行中的 Animated.timing 覆盖
● 问题:src/modal.tsx:486-488 的 onPanResponderMove 中调用 this.backdropAnimated.setValue(...) 更新滑动时的 backdrop 透明度,但未先停止 open()/close() 启动的 in-flight Animated.timing。Animated.Value.setValue() 不会停止运行中的动画:open()(line 811-815)启动的 Animated.timing 会在后续每一帧继续调用 setValue,覆盖 onPanResponderMove 设置的滑动值。
● 影响:当 modal 刚 open(backdrop 淡入动画运行中,默认 300ms)时用户立即开始滑动,backdrop 透明度会在滑动值(onPanResponderMove setValue)和淡入动画值(Animated.timing 每帧 setValue)之间反复跳动,导致 backdrop 不跟随手指平滑变化,出现抖动/回跳。同样在 close 动画期间滑动也会出现此问题。PR 注释提到 "setValue() is synchronous and works regardless of useNativeDriver setting",但未考虑到运行中的 Animated.timing 也会每帧调用 setValue 覆盖手动设置的值。
触发路径:open() 启动 backdrop 淡入 Animated.timing(300ms)→ 300ms 内用户滑动超过 panResponderThreshold → onPanResponderMove 进入 isSwipeDirectionAllowed 分支调用 setValue(swipeValue) → 同一帧/下一帧 Animated.timing 也调用 setValue(openAnimValue) → backdrop 透明度在两值之间抖动,不跟随手指。
● 建议:在滑动开始时(currentSwipingDirection 从 null 变为非 null 的分支内,line 472 赋值之前)停止 in-flight 动画:
if (!this.currentSwipingDirection) {
this.backdropAnimated.stopAnimation();
}
this.currentSwipingDirection = this.getSwipingDirection(gestureState);


修改点
本 PR 仅改动
src/modal.tsx一个文件。1. 关闭/打开时序的 TypeError 防护
isUnmounted,在componentWillUnmount()中置为true;同时把contentRef/backdropRef置空,让卸载后仍触发的回调成为 no-op。close()的setAnimation回调开头增加守卫:contentRef为空或已卸载时,直接走finalizeClose()而不去解引用contentRef.startAnimation。open()链路上的requestAnimationFrame回调、setAnimation回调、startAnimation完成回调、以及 backdrop 之后的第二个rAF回调,各自增加isUnmounted/contentRef守卫。2. 打开/关闭生命周期兜底,避免
isTransitioning卡死finalizeClose():把原先内联在startAnimation完成回调里的finalizeClose闭包提为类成员方法,供正常路径与降级路径复用。completeOpenWithoutAnimation():动画宿主不可用时,跳过内容动画直接完成打开流程(清理interactionHandle、复位isTransitioning、setState后回调onModalShow)。open()中contentRef为空、close()中contentRef为空时,分别走上述降级路径;close()的降级路径补发onModalWillHide。3.
panResponderThreshold真正生效onStartShouldSetPanResponder中移除onSwipeStart的触发(触点按下时位移为 0,此时并未开始滑动)。onPanResponderMove中,累计位移|dx|/|dy|均小于panResponderThreshold时直接 return:不移动模态、不发onSwipeStart/onSwipeMove;跨过阈值后再触发onSwipeStart。4.
supportedOrientations约束(HarmonyOS 模拟)constrainToSupportedOrientation(width, height):仅在Platform.OS === 'harmony'时生效;当前方向不被supportedOrientations支持时交换宽高。constrainDimensionsToSupportedOrientation(),在open()开头调用;handleDimensionsUpdate()也接入同一约束。5. 滑动百分比改用真实窗口尺寸
getGestureDeviceHeight()/getGestureDeviceWidth():取props.deviceHeight/Width或Dimensions.get('window'),不经过方向约束。calcDistancePercentage()的down/right分支改用这两个方法;布局仍沿用受方向约束的getDeviceWidth()/getDeviceHeight()。6.
hideModalContentWhileAnimating语义修正shouldHideContent由!showContent || (isTransitioning && !animationReady)改为!showContent || (isTransitioning && (props.hideModalContentWhileAnimating || !animationReady))。7. CodeCheck 圈复杂度整改(
15b332e)open()内层层嵌套的requestAnimationFrame/setAnimation/startAnimation回调提取为类成员方法:resetBackdropAnimation/resetContentAnimation/startOpenAnimation/handleOpenAnimationPrepared/handleOpenAnimationEnd/revealContent;open()只保留同步流程并改用提前 return 守卫;close()复用resetContentAnimation消除重复。open()圈复杂度 24 → 6,close()18 → 16。守卫条件、执行顺序、_pendingRAF入队时机均保持不变。根因分析
① 关闭模态时
cannot read property 'startAnimation' of nullclose()的原实现:this.contentRef.setAnimation(animationOut, () => { ... this.contentRef.startAnimation(...) // ← 这里 });react-native-animatable的setAnimation内部通过setState提交animationStyle,回调在该setState完成后才执行。在 RNOH 上,原生 Modal 宿主可能在这个窗口期内被平台 dismiss,React 随即把contentRef置为null;回调此时再解引用this.contentRef.startAnimation就抛 TypeError。open()链路上的setAnimation/startAnimation/requestAnimationFrame回调存在同样的时间窗口。而且这些回调还会setState,组件卸载后执行会产生 setState-after-unmount 告警。②
isTransitioning卡死导致模态彻底失效open()/close()都以if (this.isTransitioning) return;作为入口守卫,并在进入时置true,只在动画完成回调里复位为false。原实现中,
contentRef为空时:open()的if (this.contentRef) {...}整块被跳过,没有 else 分支——isTransitioning永远停在true。close()同理。一旦落入这个状态,此后所有
open()/close()调用都会被入口守卫挡掉,模态彻底不可用,且onModalShow/onModalHide再也不会触发。这比 ① 的 TypeError 影响更持久——TypeError 至少还能在日志里看见。③
panResponderThreshold声明了但从未参与判断defaultProps里有panResponderThreshold: 4,propTypes里也声明了,但代码中没有任何一处读取它。实际行为是:只要onStartShouldSetPanResponder返回 true,onSwipeStart立刻触发,随后任意微小位移都会移动模态——阈值形同虚设,轻点内容区就可能把模态拖动一下。而且
onSwipeStart被放在onStartShouldSetPanResponder(触点按下)里触发,语义也不对:按下时dx/dy都是 0,滑动尚未开始。④
supportedOrientations在 HarmonyOS 上无任何约束该 prop 在 iOS 上由原生 Modal 呈现层强制执行,在 Android 上是文档明确的 no-op,而 HarmonyOS 侧既没有原生支持也没有 JS 模拟,等于完全忽略。
⑤ 滑动百分比分母可能为负(本 PR 内的二次修正,
0c883f0)constrainToSupportedOrientation()在当前方向不被支持时会交换宽高,交换后的值经constrainDimensionsToSupportedOrientation()/handleDimensionsUpdate()写入state.deviceWidth/deviceHeight。而
calcDistancePercentage()用的gestureState.y0/moveY(及x0/moveX)是真实屏幕坐标。拿真实坐标去除以交换后的尺寸会得到错误的百分比;当触点超过交换后的边界时分母还会变负,导致newOpacityFactor > 1、遮罩透明度异常,onSwipeMove也会收到超出[0,1]的百分比。举例:400×800 竖屏 +
supportedOrientations: ['landscape']+swipeDirection: 'down',从y0 = 600下滑,(moveY - 600) / (400 - 600)得-0.5,factor 为 1.5。⑥
hideModalContentWhileAnimating被 ohos 闪屏修复覆盖为修复 RNOH 打开时的一帧闪屏,
shouldHideContent引入了!animationReady条件。但这条件把hideModalContentWhileAnimating的原语义(为 true 时整个过渡期间都隐藏内容)挤掉了:animationReady一旦为 true,即使hideModalContentWhileAnimating为 true,内容也会显示出来。方案说明(AI输出)
卸载守卫:采用「实例标志 + 置空 ref」双重防护。
isUnmounted覆盖 React 已卸载的情况,置空contentRef/backdropRef覆盖宿主被平台 dismiss 但组件尚未卸载的情况——后者正是 RNOH 上的典型场景,单靠isUnmounted挡不住。降级路径:所有守卫命中时不是简单
return,而是走completeOpenWithoutAnimation()/finalizeClose()。这一点很关键——直接 return 会把isTransitioning留在true,正好落进根因 ② 的死锁。降级路径保证isTransitioning一定复位、interactionHandle一定释放、onModalShow/onModalHide一定触发,代价只是丢掉这一次动画。finalizeClose由内联闭包提为类成员方法,是为了让正常路径与降级路径跑完全相同的收尾逻辑,避免两套实现漂移。阈值判断放在
onPanResponderMove:onStartShouldSetPanResponder阶段没有位移信息,无法做阈值判断。放在 move 里用累计dx/dy判断,同时把onSwipeStart一并挪过来,使「阈值」与「滑动开始」两个概念在时间点上对齐。阈值未跨过时整体 return,保证模态不动、回调不发,轻点内容区不会误触发滑动。supportedOrientations:限定Platform.OS === 'harmony'才生效。iOS 由原生强制执行、Android 是文档化的 no-op,JS 模拟若无平台判断会改变这两个平台的既有行为。滑动尺寸与布局尺寸分离:不回退方向约束,而是引入一组只用于手势计算的尺寸方法。布局需要受约束的几何(这正是
supportedOrientations模拟的目的),手势数学需要真实窗口空间(因为gestureState就是真实坐标)——两者本就是不同的量,分开取值比在某一处做补偿更直接,也不改变supportedOrientations的模拟行为。圈复杂度整改:严格按"提取方法"做纯结构调整,不动任何守卫条件、执行顺序和
_pendingRAF入队时机,便于 review 逐段比对。测试情况
新增测试用例(按需)
触发的已有测试用例(按需)
br_rnoh0.72分支example/src/→ Modal 示例页 → ✅ 通过手动验证
cannot read property 'startAnimation' of null。isTransitioning正确复位,后续open()/close()仍可用,onModalShow/onModalHide正常触发。panResponderThreshold生效:小于阈值的轻微拖动不移动模态、不触发onSwipeStart/onSwipeMove;跨过阈值后正常进入滑动。supportedOrientations约束下打开模态,呈现方向符合声明。supportedOrientations: ['landscape']+swipeDirection: 'down'场景下滑,onSwipeMove百分比落在[0,1],遮罩透明度正常(此前为 1.5)。hideModalContentWhileAnimating为 true 时整个过渡期间内容保持隐藏;为 false 时 ohos 闪屏防护仍生效。avoidKeyboard、customBackdrop、onBackdropPress、onBackButtonPress等行为无变化;15b332e为纯结构调整。f9d2a68修正)资料修改
panResponderThreshold与supportedOrientations在 HarmonyOS 上由"未实现"变为"已实现(JS 模拟)",README 的属性支持表建议同步更新,本 PR 暂未改动。src/modal.tsx,package.json仍为13.0.3-rc.1,未新增 CHANGELOG 条目(本仓库当前无CHANGELOG.md)。建议合入前补上版本号与变更记录。依赖关系变更
src/modal.tsx一个文件。onSwipeStart触发时机由「触点按下」推迟到「累计位移跨过panResponderThreshold」。hideModalContentWhileAnimating为 true 时,过渡期间内容保持隐藏。Checklist