Pull Request已成功合入, 合并人@zyssky465
(感谢 lizhe_ 的贡献)PickerView 高度防涨钳制会永久冻结宿主高度:一旦 lazyFrame_ 已有高度(≥80),任何 ≥ prev+20 的新帧高度都被回写成 prev 并存入 lazyFrame_,之后每帧继续满足钳制条件,合法的运行时增高永远不会生效。
场景:picker-view 高度 238px→300px(setData 驱动样式、flex 拉伸迟达、转屏),applied=300 被折回 238 且无任何路径收敛到真实高度;同时 GetPickerViewportHeight/GetWheelPadY/ETS 选中条都按 238 计算,与 Yoga 后续兄弟布局(300)错位。规格示例 style="height:300px" 的动态修改即命中。
建议:区分用户显式高度与行撑开脏高(例如要求连续 ≥2 帧同高才放行,或与 style 解析结果比对)。


should_emit 逻辑使非 immediate 模式在索引未变化时也发 change:!IsImmediateChange() 恒为 true,任意 ≥2vp 的滚动松手(包括 CANCEL 被外层 scroll 仲裁截断、以及滑回原索引的摆动)都会 CommitWheelIndex 并发 change。
规格 §2.16 规定 bindchange 在 value 改变时触发,这里同值重复触发,JS 端会收到与用户未选择动作对应的 value 提交。
建议:非 immediate 模式应要求 idx != last_emitted_index_ 才 emit;CANCEL 路径也应与 END 区分(至少不 commit 同值)。


params[0].IsString() 分支把整串交给 ParsePickerIndexList 提取数字:该方法扫描串中所有数字序列,若桥接层将对象 JSON 字符串化(如 {"range":"[["01",..."31"]]","selected":"[0,1,30]"}),range 标签里的 01..31 会被全部解析成索引,picker_selected_ 变成垃圾数组,滚轮位置与 change 事件 detail 都错。
建议:该分支改为 JSON.parse 后取 selected 字段;若对象必为结构化传递,删除该分支并加注释说明。


END 时的速度回退整体沿用最后一次 UPDATE 的速度(幅值和符号),且无符号一致性检查:快速上滑后停住手指再抬起,最后一次 UPDATE 的 +3000vp/s 被保留,滚轮会越过用户停住的项继续冲;若释放前短促反向刹车(未产生 UPDATE),则按与实测释放速度相反的旧方向 fling。
建议:幅值取 max,符号取 END 事件的符号(或至少要求两者同号才回退)。


手势期间的程序化推送不重定 pan 基线:SyncColumnWheelTranslates→ApplyWheelTranslateY 会直接改 translate,但 pan_start_translate_ 只在 ACCEPT 记录一次;手指不动时松手会把程序推入的索引提交掉(滚轮从手指下方跳走),手指一动,下一个 UPDATE 又按旧基线 pan_start_translate_+dy 打回,SettleWheel 提交的是拖动索引,与 JS 模型矛盾。
picker-view.ts 的 value observer 会在 _userPicking 期间强行推(dependent-column 复位场景),native 内部行重建(OnChildInsertedImpl)也会触发。
建议:推送时若 wheel_picking_ 为真,重定 pan_start_translate_(或推迟推送至 pickend)。


行移除路径不回补选择状态:列的行数缩小时(如日列 31→28 的 has:for splice),OnChildRemovedImpl 不重新钳制 wheel_index_/translate,也不更新 picker_selected_;与插入路径(OnChildInsertedImpl→SyncColumnWheelTranslates)不对称。
此后另一列的 change 会原样发射越界的旧索引(EmitPickerChange 无钳制),JS 端再钳回并回推,出现「事件值 ≠ 滚轮显示值」的窗口;且 JS 侧 valueExceedsRange 的 skip 会让该列长期停在越界 translate(选中条悬空)。
建议:行移除后按新行数钳制并重推 translate。


兜底 return this._findAncestorPickerView() != null 会把 picker-view 下所有非列子节点(包括内含 picker-view-column 的自定义组件包装层)从 native 树剔除:包装层宿主不建 native 节点,其内部的列因父 domNodeId 无 native 节点而永远挂载不上,滚轮空白。
但 nativePickerColumns(kids.length===1 递归)与 ETS collectPickerColumns(深度递归+isPickerColumnWrapper)都专门支持包装层,前端 light-tree 收集也能找到这些列——三层逻辑不一致。
建议:omit 前探测子树内是否含 picker-view-column,含则放行包装层,仅剔除纯非列叶子。


indicator-style 的 height 与 34/238 默认值在三个实现里各自独立解析、口径不一致:TS 正则 [0-9.]+(无单位也可)、C++ ParsePickerIndicatorItemHeight(strtof 前缀解析,无单位也可,还会命中 line-height)、ETS parsePxFromDecl(必须带 px 后缀)。
indicator-style="height:40" 时 C++/TS 按 40 排行走,ETS 高亮带和分割线回退 34——选中条比行距矮 6vp、偏移 3vp。默认值也散布 TS/C++/ETS 多处。
建议:收敛为单一解析实现(或至少统一单位语义)并共享常量。


SyncColumnWheelTranslates 对每列调用 StopWheelFling(false):fling 中途被任何程序化推送(含 native 内部行重建触发)杀死时,既不 settle 也不发 pickend。
JS 侧 pickstart 置 _userPicking=true、pickend 才清——无 pickend 时 _userPicking 永久卡 true,此后 _syncColumnsToNative 全部被跳过,range/selected 更新失效,直到用户开启下一次完整手势。
建议:StopWheelFling(false) 路径补发 pickend(或 settle 后发),保证 pickstart/pickend 配对。


新增的 ETS 状态是 C++ 滚轮的无消费镜像:syncColumnTranslates 写 FRPickerColumnView.wheelTranslateY(真实位移由 C++ NODE_TRANSLATE 负责,文件头注释也如此声明)、slotRowCount 从未读写、FlexUIRenderBaseView.pickerRowItemHeight 被 applyPickerRowItemHeight 写但全仓无读取。
同时 FRViewManager.onChildInsertedForCApi 故意不把 picker 子节点挂进 ETS 树,addSubRenderView/aboutToAppear 中的 pendingSelected 分支也基本不可达。镜像公式 pad − idx×itemH 与 C++ SyncColumnWheelTranslates 重复维护,漂移后难以排查。
建议:删除镜像与死簿记,以 C++ 为唯一事实源。


_findAncestorPickerViewColumn 与既有的 _findAncestorPickerView 是逐字节同构的 64 跳父链遍历(仅 tag 谓词不同),且经 _shouldOmitPickerNonColumn 的兜底在每次元素创建时都可能各跑一遍——不用 picker 的页面也要为每个节点付两次祖先遍历(每跳一次 elementMap 查找)。
建议:抽一个共享 _findAncestor(predicate) 助手(先用「进 picker 子树」标记或计数器短路),一次遍历同时检查两个谓词。


PickerView/PickerViewColumn 同时以硬编码特例与注册表两条路径注册:createRenderViewForCApi 的特例先于 createRenderViewFromCreator 兜底执行,会遮蔽 provider 对同名组件的自定义 creator;C++ IsCustomTsRenderView 追加的字面量 OR 永远无法改变结果(custom_ts_render_views_ 已通过 native_renderer_napi.cc 注入同名条目)。
建议:删除特例,统一走注册表。


wheel_picking_ 仅被写(cc:872/955/964)从未被读;WheelPanToken.generation 与 wheel_generation_、hub 的 running_/epoch_ 也互为冗余镜像。
文档(docs/refs/picker-view-vs-components.md)声称 wheel_picking_ 用于「commit only when panned」门控,但代码从未查询它——后续若有人按文档实现该门控会踩到与预期不符的旧值。
建议:每个生命周期只保留一个权威计数器,删除镜像。


检视结论(对照 atomic-component-spec.md §2.16/§2.17)
规格符合性 ✅
- 属性:picker-view 的
value(Array)/ indicator-style(String)/immediate-change(Boolean)名称与类型约束符合规格;picker-view-column 无独有属性,符合规格。 - 事件:
bindchange(detail:{value: number[]})/bindpickstart/bindpickend名称、bind 前缀与 detail 结构符合规格。 - 约束关系:「数字大于可选项长度时选择最后一项」的钳制在 JS/C++/ETS 三层均有实现;「只可放置 picker-view-column,其他节点不显示」由 NativeBackendElement 的 omit 机制实现;「列子节点高度自动等于选中框高度」由 resolveNativeProps/TryApplyPickerWheelFrame 实现,方向符合规格。
主要偏差 ⚠️
- 非 immediate 模式下 change 在索引未变化时也会触发(custom_ts_view.cc:962),与规格「value 改变时触发」不符。
- 另有 13 条逻辑性问题已按行号逐一 inline 评论(涉及高度钳制、手势/fling 边界、行数收缩回补、三层解析口径不一致、死代码与重复注册等),请一并处理。


跟进(新提交 2450af9f):包装层放行后 C++ 侧列收集仍只认直接子节点。
NBE 已放行内含 column 的包装层(subtreeHasPickerColumn),JS nativePickerColumns 也递归 depth 8,但这里仍只遍历 picker 的 children 里 viewType == PickerViewColumn 的直接子节点;EmitPickerChange(:960)同样只遍历直接子节点。包装层内的列会正常挂载、手势也能工作(列自身上有 pan),但:
- updatePickerRange / 行插入触发的 SyncColumnWheelTranslates 永远找不到嵌套列 → 滚轮行停在列顶、无 pad 居中;
- change 事件遍历不到任何列 → value 数组为空 → JS 端 bindchange 静默不发。
建议与 JS 对齐:收集列时递归遍历(限定深度),或至少复用 GetPickerHost 的向上查找思路做向下收集。另注意:_subtreeHasPickerColumn 依赖挂载时包装层 _childOrder 已填充,若自顶向下先 append 包装层再填子(空包装层先被 omit),嵌套列仍会回到挂不上 native 的状态——建议 omit 判定也覆盖该顺序(如 _afterPickerChildTreeChange 时对已 omit 的包装层做重评估)。


修改pickerview子组件渲染方式;解决pickerview页面不能滑动问题