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.ts
    • flexui-engine/.../adapters/picker-view-column-adapter.ts
    • Native:FRPickerWheelView.ets(每列 clip Stack + C++ HandleGestureBySelf + NODE_TRANSLATE + ContentSlot;非 ArkUI Scroll / 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

实现要点

  1. 列与行:picker-view 只展示 picker-view-column。其它光树节点不建 native;子树内含 column 的包装层(自定义组件)必须建 native,否则列挂不上、滚轮空白。picker-view-inner 与纯非列叶子仍 omit;空自定义包装层先放行(子列后挂),避免自顶向下空壳被 omit。column 模板为 <slot />,直接子节点就是滚轮行。行由 C++ CustomTsView::OnChildInsertedImpl 插入列的 ContentSlot。JS nativePickerColumns 与 C++ CollectPickerColumns 均递归收集列(深度 8,含包装层);SyncColumnWheelTranslates / EmitPickerChange / ColumnIndexInPicker 走同一收集,嵌套列也能 pad 居中与 bindchange。Omit 判定靠 _inPickerSubtree / _inPickerColumn 短路;_findAncestorPickerView 走共享 _findAncestor(pred)。
  2. 滚轮几何:每列 Stack(clip + 热区)→ FRPickerColumnSlotBody(ContentSlot,无 @State)+ C++ NODE_TRANSLATE。不用 ArkUI Scroll。点 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。视口 responseRegion 100%,行 HIT_TEST_MODE_NONE。甩动手势:松手后按 pan 速度做减速惯性,停稳再吸附;pickend 在惯性结束之后发。END 速度回退仅在与最后一次 UPDATE 同号时取较大幅值,符号跟 END:停住再抬起不沿用旧速度冲过当前项,反向刹车不按旧方向 fling。
  3. 选中只认滑动: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 标签当索引。
  4. 点击 item:pan ACCEPT 发 pickstart。位移² < 4 视为轻点:不根据点击 Y 算行、不跳到该行;松手立即 pickend,不发 change。要对齐微信「点某一行滚到中间」,需另做 tap→index,当前未做。
  5. immediate-change:true 时拖动/惯性中 index 变就 change,停稳若最终 index 不同则补发;false 时仅停稳吸附且 index 相对本次手势起点已变 才发 change。有位移才进入惯性,否则立即吸附。外层 scroll 截断(CANCEL)回落到上次已提交索引,不发 change。程序化 StopWheelFling(false) 掐掉惯性时仍发 pickend,保证与 pickstart 配对(否则 JS _userPicking 卡死、range 同步失效)。
  6. 受控 value:手势中 _userPicking 拦住把旧 data.value 灌回 native。父组件按钮 setData({ value }) 会清 _userPicking 并下发(打断手势保护,便于外部强制跳转)。change 回声的同值 value 不下发强制跳转,由 C++ SyncColumnWheelTranslates 保持当前吸附,避免跟手势打架。ETS 不镜像 wheelTranslateY(位移唯一事实源在 C++)。
  7. 动态列长(如日 31→28):has:for 增删走真实 insert/remove。glass-easel spliceRemove 的 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 也按各列当前行数钳制,避免选中条悬空或事件值越界。
  8. 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 无滚轮,固定高度空容器。
  9. 与 <picker> 区分:<picker> 为 tap 弹层(FRPickerView.ets Dialog);picker-view 为页面内滚轮,勿复用弹层逻辑。
  10. 验证:改 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)