picker-view — 实现与 components.md 对照
对照基准:
- 规格:ASCF
picker-view/picker-view-column(官方属性表;atomic-component-spec §2.16) - 实现:
packages/flexui-frontend/src/components/picker-view/packages/flexui-frontend/src/components/picker-view-column/flexui-engine/.../adapters/picker-view-adapter.tsflexui-engine/.../adapters/picker-view-column-adapter.ts- Native:
FRPickerWheelView.ets(每列 clip Stack + C++ HandleGestureBySelf + NODE_TRANSLATE + ContentSlot;非 ArkUIScroll/List、非TextPicker、非<picker>弹层) - C++:
CustomTsView::OnChildInsertedImpl(列的直接子行插入 ContentSlot,不buildFlexUIRenderView重画) - Demo:
demos/ascf-ai-demo/.../picker-view-demo/
模板 kebab-case;组件 properties camelCase;adapter / host attribute 仅 kebab-case。
包名说明:内置组件在
@openflexui/frontend;adapter 在flexui-backend。
结论摘要
| 维度 | 结论 |
|---|---|
| picker-view 属性 | value、indicator-style、immediate-change(官方 §2.16) |
| picker-view 事件 | change / pickstart / pickend 由列 C++ pan + 惯性 贯通 |
| 点击行 | 不改变选中:轻点 pickstart → 立即 pickend,不发 change;不会把该行滚进选中条 |
| picker-view-column | 无属性;模板只有 <slot />;仅此组件可作为子列;其它光树节点不显示 |
| 形态 | JS-logic builtin(NATIVE_JS_LOGIC_BUILTINS);column 参与 Yoga 分宽,行走 slot 插入 |
| Native | adapter name: 'PickerView' / PickerViewColumn;ETS-only(OHOS);ETS_ONLY_RENDER_VIEWS |
| Web / Domlike | 暂无滚轮渲染::host 高 238px 空容器;inner / column 不在 Web 端画轮 |
属性对照
| 属性(模板) | 官方规格 | 当前实现 |
|---|---|---|
value |
Array<number>,越界选最后一项 |
✅ clamp 后同步 native selected |
indicator-style |
string,选中框样式;Skyline 支持 height / border / background-color |
✅ height→行高:TS / C++ / ETS 同口径(独立 height,正数,可选 px;默认 34;不认 line-height / % / rpx);border→divider;background-color 铺整行一条(不按列分框) |
immediate-change |
boolean,默认 false;开则滚动中 index 变就 change,关则惯性停稳后 change | ✅ C++ pan / fling 分支 |
indicator-class |
不在官方属性表(§2.16 无此项) | ⏭️ 不实现、不声明 |
mask-style |
不在官方属性表 | ⏭️ 不实现、不声明 |
range / bindcolumnchange |
非 picker-view 规格 | ⏭️ 不实现、不声明;列数据来自 column 光树子节点 |
Boolean:immediate-change
与框架 isAttrPresenceOn(attr_bool.ts / AttributeNormalizer)一致:仅 false / 0 / '0' / 未传为关;字符串 "false" 仍为 true(对齐微信异常用例)。组件层 _syncBoolAttr 只写 'true' 或 removeAttribute;adapter / native parseBooleanProp 同语义。
事件对照
| 事件 | 官方规格 | 当前实现 |
|---|---|---|
bindchange |
event.detail = { value: number[] } |
✅ 仅 value 变了才发(吸附后 idx != last_emitted;immediate-change 为真时拖动/惯性中 index 变也会发);轻点、滑回原索引、外层 scroll CANCEL 不发 |
bindpickstart |
滚动选择开始 | ✅ 列 C++ pan ACCEPT |
bindpickend |
滚动选择结束 | ✅ 轻点:松手立即发;有滑动:惯性停稳吸附后发 |
bindtap |
adapter 映射 tap → click |
⚠️ 映射在;点击某一行不会选中该行、不改 value |
实现要点
- 列与行:
picker-view只展示picker-view-column。其它光树节点不建 native;子树内含 column 的包装层(自定义组件)必须建 native,否则列挂不上、滚轮空白。picker-view-inner与纯非列叶子仍 omit;空自定义包装层先放行(子列后挂),避免自顶向下空壳被 omit。column 模板为<slot />,直接子节点就是滚轮行。行由 C++CustomTsView::OnChildInsertedImpl插入列的 ContentSlot。JSnativePickerColumns与 C++CollectPickerColumns均递归收集列(深度 8,含包装层);SyncColumnWheelTranslates/EmitPickerChange/ColumnIndexInPicker走同一收集,嵌套列也能 pad 居中与bindchange。Omit 判定靠_inPickerSubtree/_inPickerColumn短路;_findAncestorPickerView走共享_findAncestor(pred)。 - 滚轮几何:每列
Stack(clip + 热区)→FRPickerColumnSlotBody(ContentSlot,无@State)+ C++NODE_TRANSLATE。不用 ArkUIScroll。点 set 只改translate.y,不得重建 ContentSlot,否则 Native 子节点 0×0,C++setAttribute报 106103(ARKTS_NODE_NOT_SUPPORTED),外层scroll-view锁死。C++ 对 picker 子树 不写入 0 宽高,沿用上次有效 frame。列 Yoga 高度锁成 picker 视口 px(不能height:100%被 has:for 行撑开),否则外层scroll-view内容高度后涨、回顶并失效。picker-view 宿主高度以 style 确定 px 为准:行撑开的脏高(≈ 行数×itemH)不得抬升视口,但setData/ 转屏把height从 238 改到 300 必须生效,不得把lazyFrame_永久冻在首帧。列 Native Column 走文档流,不用SetPosition(0,0)(脱离文档流后 Scroll/clip 量高为 0,轮子全白)。set 改 translate、改indicator-style改行高都不得改变视口排版高。居中偏移并进translate(不用.padding())。公式:offsetY = index × itemHeight;translate.y = pad − offsetY,pad = (pickerH − itemH) / 2。itemHeight来自indicator-style的height(默认 34)。选中条 / 分割线HitTestMode.None。视口responseRegion100%,行HIT_TEST_MODE_NONE。甩动手势:松手后按 pan 速度做减速惯性,停稳再吸附;pickend在惯性结束之后发。END 速度回退仅在与最后一次 UPDATE 同号时取较大幅值,符号跟 END:停住再抬起不沿用旧速度冲过当前项,反向刹车不按旧方向 fling。 - 选中只认滑动:
CommitWheelIndex只从 pan / fling / settle 路径调用(点按不改value)。wheel_picking_表示本列手势会话(ACCEPT→pickend):①SyncColumnWheelTranslates跳过正在 pan/fling 的列;② 会话中推送改translate时重定pan_start_translate_;③ 掐 fling / CANCEL / settle 时配对pickend。程序化value/ range 跳转直接设translate.y。pan 回调用wheel_generation_(token 只存注册快照)+weak_ptr防 UAF;fling hub 只用epoch_作生命周期计数。has:for行插入要 debounce 成一次 jump。不换.key、不销毁列。updatePickerRange只读selected(数字数组或该字段的 JSON);不得扫描整包字符串里的 range 标签当索引。 - 点击 item:pan ACCEPT 发
pickstart。位移² < 4 视为轻点:不根据点击 Y 算行、不跳到该行;松手立即pickend,不发change。要对齐微信「点某一行滚到中间」,需另做 tap→index,当前未做。 - immediate-change:
true时拖动/惯性中 index 变就change,停稳若最终 index 不同则补发;false时仅停稳吸附且 index 相对本次手势起点已变 才发change。有位移才进入惯性,否则立即吸附。外层 scroll 截断(CANCEL)回落到上次已提交索引,不发 change。程序化StopWheelFling(false)掐掉惯性时仍发pickend,保证与pickstart配对(否则 JS_userPicking卡死、range 同步失效)。 - 受控 value:手势中
_userPicking拦住把旧data.value灌回 native。父组件按钮setData({ value })会清_userPicking并下发(打断手势保护,便于外部强制跳转)。change回声的同值value不下发强制跳转,由 C++SyncColumnWheelTranslates保持当前吸附,避免跟手势打架。ETS 不镜像wheelTranslateY(位移唯一事实源在 C++)。 - 动态列长(如日 31→28):
has:for增删走真实 insert/remove。glass-easelspliceRemove的deleteCount是 for-item 个数;JS_childOrder里是 VirtualNode + 展开行(V+M)成对。NativeBackendElement._removeRun/spliceBefore按逻辑兄弟收集,展开行不占用deleteCount,否则会少删一天(28 后面仍见 31)。C++ 列 slot 内是参与高度的ColumnNode(不用SetPosition(0,0),否则删行后高度不收缩)。行移除后按新行数钳制wheel_index_/translate/picker_selected_(与插入路径对称);change发出的value也按各列当前行数钳制,避免选中条悬空或事件值越界。 - ETS-only chrome:
FRPickerWheelView只画选中条/分割线与 ContentSlot 壳;indicator-style驱动pickerItemHeight/ divider / bg。inner / column 只参与 Yoga 分宽,不作为 FRDiv 再画一遍。FRViewManager.createRenderViewForCApi不硬编码 Picker,走 provider creator →FlexUIRenderRegisterMap;C++IsCustomTsRenderView认custom_ts_render_views_(ETS_ONLY_RENDER_VIEWS/ napi 注入),无 Picker 字面量 OR。FRViewManager的 C-API 子节点插入不把 picker 行挂进 ETS 树(行走 ContentSlot)。Web / Domlike 无滚轮,固定高度空容器。 - 与
<picker>区分:<picker>为 tap 弹层(FRPickerView.etsDialog);picker-view为页面内滚轮,勿复用弹层逻辑。 - 验证:改 adapter /
NativeBackendElement→build-jsfwk;改 ETS / C++ →build-engine;改 frontend CSS/hxml →compile-builtins.mjs+pnpm build。
Demo 覆盖
| 块 | 覆盖 |
|---|---|
| 正向 | 三列 has:for、value 受控、change / pickstart / pickend |
| 正向 | 单列(时 0–23)、双列(时/分) |
| 正向 | 自定义行:色块 view + 标题/副标题(城市);静态嵌套 view 行(非 has:for,配送)+ 一列纯 text |
| 正向 | indicator-style 切换(高度 / 边框 / 背景色)、immediate-change 开/关 |
| 正向 | 动态列长(日列 31 → 28 与 28 → 31,列数不变;缩列后不应残留 31) |
| 正向 | 局部 scroll-view 套 picker-view:demo 内固定高度 420px 的 nest。Demo 根是普通 view,不要再套一层 height:100% 的纵向 scroll-view。 |
| 正向 | 宿主高度 238→300(style="height: {{hostHeight}}px",绿底 #b8f0c8):setData 增高必须生效;28 行不得把视口撑到 ~952。 |
| 异常 | value 错类型、越界 clamp、空 []、长度不足、负数、小数 |
| 异常 | immediate-change="false" → 仍为开(isAttrPresenceOn) |
| 异常 | picker-view 内夹 view/text:不显示,只留 column |
当前 不验收「点击非中间行即选中该行」。验收选中请 滑动 列,看选中条与 bindchange 的 value。
关键文件
| 角色 | 路径 |
|---|---|
| 组件 | packages/flexui-frontend/src/components/picker-view/ |
| 列 | packages/flexui-frontend/src/components/picker-view-column/(<slot />) |
| Adapter | flexui-engine/driver/js/packages/flexui-backend/src/adapters/picker-view-adapter.ts |
| 列 adapter | .../adapters/picker-view-column-adapter.ts |
| 列表 diff | .../native-backend/NativeBackendElement.ts(spliceRemove 逻辑兄弟) |
| ETS 滚轮 | .../pickerview/FRPickerWheelView.ets |
| C++ slot | custom_ts_view.cc(OnChildInsertedImpl / 列 ColumnNode) |
| Demo | demos/ascf-ai-demo/.../picker-view-demo/(嵌入 complex,根是 view) |