一、为什么有两套 UI?先分清分工

很多南向开发者第一次看源码会困惑:明明已经有 UIView 了,为什么还要 Component?

表格

维度ace_engine_liteui_lite
定位JSUI 应用框架(前端运行时)轻量级图形渲染引擎(后端绘制)
语言C++ + JerryScript(JS 引擎)纯 C++
抽象层级声明式:new Text('hello')命令式:label->SetText("hello")
职责解析 JS、管理组件树、计算样式/布局、事件分发调度建一棵 UIView 树、标脏、Draw、上屏
生命周期随 JS Page 创建/销毁随 RootView 挂载,帧驱动刷新
你改哪层加新组件、改事件模型、改样式属性加新控件、改绘制效果、改输入处理

关键认知:ace_engine_lite 的 Component 不是 UIView 的子类,它是 UIView 的持有者与管理者。一个 Component 内部可以持有多个 UIView(比如一个自定义组件包含一个背景 View + 一个文字 View),但对外只暴露一个根节点 —— GetComponentRootView()。

┌───────────── JS 层 ─────────────┐
   new Button({text:'OK'})
              │  createElement
┌───────── ace_engine_lite ───────┐
   ButtonComponent  ◄──► nativeElement_ (JerryScript 对象)
          │ Create()
┌────────── ui_lite ──────────────┐
   UILabelButton  ──Draw──►  buffer ──► 屏幕

⚠️ 这条链路上任何一环断了,表现都是「页面空白但没报错」——因为 JS 层异常往往被框架吞掉,只能靠逐层打点定位。


二、组件映射表:ComponentFactory 的注册表

ComponentFactory 是整条链路的唯一入口。JS 里 createElement('button') 最终会走到这个工厂的 switch/case 或注册表,返回对应的 Component,Component 的 Create() 再去 new 出 UIView。

表格

ace_engine_lite Componentui_lite UIView对应关系与实现要点
DivComponentUIViewGroup最基础的容器,对应 div;只负责排布子节点,自己不画东西
StackComponentUIViewGroup层叠容器,子节点按添加顺序 Z 序叠加;注意后 Add 的在上层
TextComponentUILabel文本显示;字体/字号/颜色走 Style 解析,缺字是字库裁剪问题
ImageComponentUIImageView图片;src 路径由资源管理器转成 pixelmap,大图解码是 OOM 高发点
ListComponentUIList可滚动列表;必须开复用,否则长列表必卡
SwiperComponentUISwipeView滑动翻页;snap to page,loop 模式首尾衔接需实测
SliderComponentUISlider滑块;拖动事件要消费掉,避免穿透到下层滚动容器
SwitchComponentUIToggleButton开关;状态切换后记得 Invalidate()
CheckboxComponentUICheckBox复选框
RadioComponentUIRadioButton单选;互斥逻辑在 Component 层(RadioGroup)实现,UIView 只管画
ButtonComponentUIButton / UILabelButton有图用 UIButton,纯文字用 UILabelButton
InputEditTextComponentUIEditText输入框;焦点全局唯一,切焦点要收起旧键盘/停光标动画
ProgressComponentUIBoxProgress / UICircleProgress横向条形 / 环形;方向由属性决定
ChartComponentUIChart图表;能力基础,点数多会卡,大数据量建议自绘
CanvasComponentUICanvas画布;给 JS 暴露自绘入口
PickerComponentUIPicker选择器;多列联动时避免回调里同步刷新导致递归
AnalogClockComponentUIAnalogClock模拟时钟;指针旋转常用 UITextureMapper 配合
VideoComponentUIVideo视频容器;实际解码播放走多媒体子系统
MarqueeComponentUILabel(marquee 模式)跑马灯;本质是 UILabel + 逐帧偏移动画
ScrollLayerUIViewGroup + UIScrollView 包裹页面级滚动层;整个 Page 内容包在一个 ScrollView 里

三、对接机制核心:三层双向绑定

这是本节最重要的部分,也是你能在源码里抓住的唯一一根「线」。

3.1 代码锚点

// component.cpp:164-165(行号随版本变动,以你本地为准)
jerry_set_object_native_pointer(nativeElement_, this, nullptr);

这一行把三样东西缝在了一起:

┌─────────────────────────────────────────────┐
│  jerry_value_t nativeElement_  (JS 对象)     │  ← JS 侧拿到的 element
│         ↕ jerry_set_object_native_pointer    │
│  Component* this             (C++ 对象)      │  ← 逻辑与生命周期管理者
│         ↕ GetComponentRootView()             │
│  UIView* rootView_           (渲染节点)      │  ← 树上真实存在的那一个
└─────────────────────────────────────────────┘

3.3 「双向绑定」的工程含义

表格

特性具体表现风险
三层强耦合任意一层悬空 = 崩溃或静默失效销毁顺序必须是 JS 解绑 → Component 析构 → UIView 从父节点 Remove → delete
原生指针无所有权语义void* 不携带生命周期信息dangling pointer;UIView 活着但 Component 已死,一点就崩
无 GC 联动finalize 为 nullptr必须在 Page/组件 onDestroy 里成对清理
跨层访问违规在非 UI 线程通过 elementPtr 调 JS APIJerryScript 非线程安全,直接 abort;所有 UI 操作收拢 UI 线程(ui_lite 的硬约束,这里同样适用)

四、完整链路走一遍:从 new Button() 到屏幕上出现一个按钮

4.1 创建阶段

1. JS:  new Button({text: 'OK', x: 10, y: 20})
2. ace: createElement('button') → ComponentFactory::Create(TAG_BUTTON)
3. ace: new ButtonComponent() → component->Create()
4. ui:  new UILabelButton()                          ← UIView 诞生
5. ace: component->SetUIView(labelButton)
6. ace: jerry_set_object_native_pointer(nativeElement_, this, nullptr)
7. ui:  rootView_->Add(labelButton)                  ← 上树,从此每帧被 Draw
8. ui:  labelButton->Invalidate()                    ← 首帧标脏

第 7 步之前,UIView 画了也白画(不在树上,RootView 遍历不到)。
第 6 步漏了,事件回不来。这两条是排查新建组件不显示/不响应的唯二检查点。

4.2 属性更新阶段(setState 之后)

JS setState
 → ace  diff 属性(Style/Attr 对比)
 → Component::Update(attr)          ← 只更新变化的属性
 → UIView->SetText() / SetPosition() ...
 → UIView->Invalidate()             ← ★ 必须标脏,否则画面不动
 → 下一帧 RootView::Draw(脏区)

4.3 事件回流阶段(最容易断的一环)

触摸按下
 → InputManager::DispatchEvent
 → RootView::DispatchTouchEvent
 → UIViewGroup::DispatchEvent(逆序遍历子节点,上层优先)
 → UIView::OnEvent(event)
    ├─ hit test:点是否在 rect 内
    ├─ 记录按下态 → Invalidate()(视觉反馈)
    └─ 抬起时:
        if (onClickListener_) onClickListener_->OnClick(this);
           ↓
        ViewExtraMsg.elementPtr → nativeElement_
           ↓
        CallJSFunction(nativeElement_, "onClick", args)   ← 跨到 JS
           ↓
        JS 的 onClick() 执行

五个高频断点:

  1. OnEvent 返回了 false → 事件继续冒泡,被父容器吃掉(滚动容器尤其容易)。可交互控件消费事件必须 return true。
  2. hit test 坐标算错 → 相对坐标/绝对坐标混用,或者父容器有 scroll offset 没补偿。
  3. 容器没裁剪也没消费 → 子控件画出界,点在可视区域外却还能点到。
  4. elementPtr 为空 → 跨层回调找不到 JS 对象,表现为「UI 有反应(按下态变了)但 JS 回调没执行」。
  5. JS 回调里抛异常 → JerryScript 异常若不检查 jerry_value_is_error,后续事件全部静默丢失。

五、动手:给系统加一个自定义组件(完整对照模板)

假设要加一个 Knob(旋钮)组件,需要在两层各写一份代码,并在工厂注册。

5.1 ui_lite 侧:UIView 子类(负责画 + 响应触摸)

// foundation/arkui/ui_lite/frameworks/components/ui_knob.h
class UIKnob : public UIView {
public:
    UIKnob() { SetWidth(80); SetHeight(80); }
    void SetValue(int v);
    int GetValue() const { return value_; }
    using OnChanged = void (*)(UIKnob&, int);
    void SetOnChanged(OnChanged cb) { onChanged_ = cb; }

    bool OnEvent(const Event& event) override;
    void Draw(const Rect& area) override;

private:
    int value_ = 0;
    OnChanged onChanged_ = nullptr;
};

要点回顾:属性改了必 Invalidate();Draw 先做矩形相交裁剪;拖拽消费事件 return true。(细节见社区《ui_lite 组件层详解》的自定义组件模板)

5.2 ace_engine_lite 侧:Component 子类(负责接 JS 参数 + 持有 UIView)

// foundation/ace/ace_engine_lite/frameworks/components/knob_component.h
class KnobComponent : public Component {
public:
    explicit KnobComponent(const char* name) : Component(name) {}
    ~KnobComponent() override = default;

    // ★ 核心:创建并返回根 UIView
    UIView* GetComponentRootView() const override { return rootView_; }

    // ★ 接收 JS 侧属性
    bool SetPrivateAttribute(const char* attrName, jerry_value_t attrValue) override;

    // ★ 事件回传到 JS
    void OnValueChanged(int value);

private:
    UIKnob* rootView_ = nullptr;
};
// knob_component.cpp
UIView* KnobComponent::CreateView()
{
    rootView_ = new UIKnob();
    // 建立反向绑定(字段名以你本地 ViewExtraMsg 定义为准)
    auto* extra = rootView_->GetViewExtraMsg();
    if (extra != nullptr) {
        extra->elementPtr = &nativeElement_;   // ★ 指向 nativeElement_ 的地址
    }
    return rootView_;
}

bool KnobComponent::SetPrivateAttribute(const char* attrName, jerry_value_t attrValue)
{
    if (strcmp(attrName, "value") == 0) {
        int v = static_cast<int>(jerry_get_number_value(attrValue));
        if (rootView_ != nullptr) {
            rootView_->SetValue(v);            // 只改属性,Create 后才能调
        }
        return true;
    }
    return Component::SetPrivateAttribute(attrName, attrValue);
}

void KnobComponent::OnValueChanged(int value)
{
    // ★ 回调 JS:通过 nativeElement_ 找到 JS 侧绑定的 onchanged
    CallJSMethod(nativeElement_, "onchanged", value);  // 框架封装,注意错误检查
}

5.3 注册到工厂

// component_factory.cpp
Component* ComponentFactory::CreateComponent(const char* componentName, ...)
{
    if (strcmp(componentName, "knob") == 0) {
        auto* comp = new KnobComponent(componentName);
        comp->CreateView();          // 有的版本在 Add 时懒创建,按你本地写法来
        return comp;
    }
    // ...原有分支
}

验证三连(缺一不可)

// JS 侧
import router from '@system.router';
export default {
    render() { /* hml 里 <knob value="30" onchanged="onTurn"></knob> */ },
    onTurn(e) { console.info('knob: ' + e.value); }
}
  1. 显示:屏幕出现旋钮 → 工厂注册 + CreateView + Add 到树都通了。
  2. 更新:改 value 属性画面跟着变 → diff → SetPrivateAttribute → Invalidate 通了。
  3. 交互:拖动打印日志 → OnEvent 消费 + elementPtr 反向绑定 + JS 回调通了。

 

 


六、边界:什么不该在哪层做

想做的事正确归属
新增一个控件的视觉效果ui_lite 新增 UIView 子类
新增一个 JS 可调用的组件/属性ace 新增 Component + 工厂注册 + 属性解析
改交互逻辑(互斥、联动、数据绑定)ace 层 Component
改绘制原语(画圆/画字/抗锯齿)ui_lite 的 draw/ 模块
改布局算法ui_lite 的 layout/(FlexLayout 等)+ ace 的布局计算
改帧率、双缓冲、上屏合成window_manager_lite / surface / engines
跨进程通信、状态持久化不属于这两层,走应用框架
Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐