已关闭
fix: 补充close/open链卸载守卫与兜底;panresponderThreshold阈值生效;supportedOrientations朝向约束 #33
fix: 补充close/open链卸载守卫与兜底;panresponderThreshold阈值生效;supportedOrientations朝向约束 #33
已关闭
mazheng创建于 8月31日关闭于 22 天前
mazheng
8月31日

修改点

本 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 null

close() 的原实现:

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 逐段比对。


测试情况

本仓库未引入自动化单测框架(无 __tests__ / *.test.*),验证以 example 工程 + 真机手动执行为主。

新增测试用例(按需)

  • 不涉及(本仓库无自动化单测工程)

触发的已有测试用例(按需)

  • 本工程 br_rnoh0.72 分支 example/src/ → Modal 示例页 → ✅ 通过
  • RNT 工程 → 待补充(如未触发请填"不涉及")

手动验证

  • 已验证修复有效:
    • 反复快速开关模态、以及在关闭动画过程中卸载页面,不再出现 cannot read property 'startAnimation' of null。
    • 动画宿主不可用的降级场景下,isTransitioning 正确复位,后续 open() / close() 仍可用,onModalShow / onModalHide 正常触发。
    • panResponderThreshold 生效:小于阈值的轻微拖动不移动模态、不触发 onSwipeStart / onSwipeMove;跨过阈值后正常进入滑动。
    • supportedOrientations 约束下打开模态,呈现方向符合声明。
    • 400×800 竖屏 + supportedOrientations: ['landscape'] + swipeDirection: 'down' 场景下滑,onSwipeMove 百分比落在 [0,1],遮罩透明度正常(此前为 1.5)。
    • hideModalContentWhileAnimating 为 true 时整个过渡期间内容保持隐藏;为 false 时 ohos 闪屏防护仍生效。
  • 已确认不影响其他功能:avoidKeyboard、customBackdrop、onBackdropPress、onBackButtonPress 等行为无变化;15b332e 为纯结构调整。
  • CI:编译 ✅ / 静态检查(CodeCheck)✅ / DCO ✅(首次提交未签名,已在 f9d2a68 修正)

资料修改

  • 是否已在README说明该接口/功能的变化:否。panResponderThreshold 与 supportedOrientations 在 HarmonyOS 上由"未实现"变为"已实现(JS 模拟)",README 的属性支持表建议同步更新,本 PR 暂未改动。
  • 是否已在README和package.json修改了版本信息:否。本 PR 只改动了 src/modal.tsx,package.json 仍为 13.0.3-rc.1,未新增 CHANGELOG 条目(本仓库当前无 CHANGELOG.md)。建议合入前补上版本号与变更记录。

依赖关系变更

  • 无新增依赖,无产物变更(本库为纯 JS 实现,不含 harmony 原生模块与 har)。
  • 改动范围仅 src/modal.tsx 一个文件。
  • 兼容性:无 API 签名变更、无 breaking change。以下为向上游/文档语义对齐的行为修正,依赖旧行为的业务代码需留意:
    • onSwipeStart 触发时机由「触点按下」推迟到「累计位移跨过 panResponderThreshold」。
    • 小于阈值的拖动不再移动模态。
    • hideModalContentWhileAnimating 为 true 时,过渡期间内容保持隐藏。

Checklist

likedislike
当前Pull Request已关闭, 关闭人@mazheng
openharmony_ci
openharmony_ci成员
8月31日 评论:

感谢提交 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:

likedislike
openharmony_ciopenharmony_ci成员
8月31日 添加了label:dco检查失败
Mmazheng
9月1日 强制推送  1 个提交:0f34a0b3-fix: 补充close/open链卸载守卫与兜底;panresponderThreshold阈值生效;supportedOrientations朝向约束
openharmony_ci
openharmony_ci成员
9月1日 评论:

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

likedislike
openharmony_ciopenharmony_ci成员
9月1日 删除了label:dco检查失败
此处折叠了106条消息 查看更多
openharmony_ciopenharmony_ci成员
23 天前 删除了label:编译成功
openharmony_ciopenharmony_ci成员
23 天前 删除了label:静态检查成功
openharmony_ci
openharmony_ci成员
23 天前 评论:

代码有更新,重置PR验证状态

likedislike
openharmony_ci
openharmony_ci成员
23 天前 评论:

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

likedislike
Mmazheng
22 天前 关闭了 pull request