1# JS UI开发框架新增组件开发指南:以新增MyCircle组件为例
2
3本篇wiki将通过新增一个MyCircle组件为例,向大家展示新增一个JS UI组件的全流程。
4
5完整的patch链接:https://gitee.com/theretherehuh/ace_ace_engine/pulls/1/files
6
7
8
9### mycircle
10
11可点击的展示类组件,展示一个圆,支持设置半径、边缘宽度和边缘颜色,可以通过点击事件获得当前圆的半径和边缘宽度。
12
13#### 支持设备
14
15| 手机 | 智慧屏 | 智能穿戴 | 轻量级智能穿戴 | 轻车机 |
16| ---- | ------ | -------- | -------------- | ------ |
17| 支持 | 支持   | 支持     | 支持           | 支持   |
18
19#### 子组件
20
2122
23#### 属性
24
25| 名称         | 属性类型 | 默认值 | 必填 | 描述       |
26| ------------ | -------- | ------ | ---- | ---------- |
27| circleradius | length   | 20vp   | 否   | 默认半径。 |
28
29#### 样式
30
31| 名称       | 属性类型     | 默认值  | 必填 | 描述                 |
32| ---------- | ------------ | ------- | ---- | -------------------- |
33| circleedge | length color | 2vp red | 否   | 默认边缘颜色和宽度。 |
34
35#### 事件
36
37| 名称        | 参数类型                                              | 描述                                                         |
38| ----------- | ----------------------------------------------------- | ------------------------------------------------------------ |
39| circleclick | {radius: circle radius, edgewidth: circle edge width} | 点击MyCircle组件时触发该回调,返回当前circle的半径和边缘宽度,单位是px。 |
40
41#### 示例
42
43```typescript
44<!-- xxx.hml -->
45<div style="flex-direction: column;align-items: center;">
46    <text>"MyCircle的半径为:{{radiusOfMyCircle}}"</text>
47    <text>"MyCircle的边缘宽度为:{{edgeWidthOfMyCircle}}"</text>
48    <mycircle  circleradius="40vp" style="circleedge: 2vp red;" @circleclick="onCircleClick"> </mycircle>
49</div>
50```
51
52```js
53// xxx.js
54export default{
55    data:{
56        radiusOfMyCircle: -1,
57        edgeWidthOfMyCircle: -1,
58    },
59    onCircleClick(event) {
60        this.radiusOfMyCircle = event.radius
61        this.edgeWidthOfMyCircle = event.edgewidth
62    }
63}
64```
65
66该界面最终效果如下图所示:
67
68![](https://gitee.com/theretherehuh/ace_ace_engine/raw/resources/mycircle.gif)
69
70
71
72### 1. `js`的界面解析
73
74#### 1.1 `dom_type`中增加新组件的属性定义
75
76##### 1.1.1 在`dom_type.h`中增加`MyCircle`的属性定义
77
78文件路径为:`frameworks\bridge\common\dom\dom_type.h`
79
80```c++
81// node tag defines
82/* .................................... */
83/* node tag defines of other components */
84/* .................................... */
85ACE_EXPORT extern const char DOM_NODE_TAG_MYCIRCLE[];
86
87/* ........................... */
88/* defines of other components */
89/* ........................... */
90
91// mycircle defines
92ACE_EXPORT extern const char DOM_MYCIRCLE_CIRCLE_EDGE[];
93ACE_EXPORT extern const char DOM_MYCIRCLE_CIRCLE_RADIUS[];
94ACE_EXPORT extern const char DOM_MYCIRCLE_CIRCLE_CLICK[];
95```
96
97&nbsp;
98
99##### 1.1.2 在`dom_type.cpp`中增加`MyCircle`的属性值
100
101文件路径为:`frameworks\bridge\common\dom\dom_type.cpp`
102
103```c++
104// node tag defines
105/* .................................... */
106/* node tag defines of other components */
107/* .................................... */
108const char DOM_NODE_TAG_MYCIRCLE[] = "mycircle";
109
110/* ........................... */
111/* defines of other components */
112/* ........................... */
113
114// mycircle defines
115const char DOM_MYCIRCLE_CIRCLE_EDGE[] = "circleedge";
116const char DOM_MYCIRCLE_CIRCLE_RADIUS[] = "circleradius";
117const char DOM_MYCIRCLE_CIRCLE_CLICK[] = "circleclick";
118```
119
120&nbsp;
121
122####  1.2 新增`DOMMyCircle`类
123
124##### 1.2.1 新增`dom_mycircle.h`
125
126文件路径:`frameworks\bridge\common\dom\dom_mycircle.h`
127
128```c++
129class DOMMyCircle final : public DOMNode {
130    DECLARE_ACE_TYPE(DOMMyCircle, DOMNode);
131
132public:
133    DOMMyCircle(NodeId nodeId, const std::string& nodeName);
134    ~DOMMyCircle() override = default;
135
136    RefPtr<Component> GetSpecializedComponent() override
137    {
138        return myCircleChild_;
139    }
140
141protected:
142    bool SetSpecializedAttr(const std::pair<std::string, std::string>& attr) override;
143    bool SetSpecializedStyle(const std::pair<std::string, std::string>& style) override;
144    bool AddSpecializedEvent(int32_t pageId, const std::string& event) override;
145
146private:
147    RefPtr<MyCircleComponent> myCircleChild_;
148};
149```
150
151`DOMMyCircle`继承自`DOMNode`,主要功能是解析界面并生成相应的`Component`节点。
152
153&nbsp;
154
155##### 1.2.2   新增`dom_mycircle.cpp`
156
157文件路径:`frameworks\bridge\common\dom\dom_mycircle.cpp`
158
159**一、组件属性的解析:`SetSpecializedAttr`**
160
161```c++
162bool DOMMyCircle::SetSpecializedAttr(const std::pair<std::string, std::string>& attr)
163{
164    if (attr.first == DOM_MYCIRCLE_CIRCLE_RADIUS) { // "circleradius"
165        myCircleChild_->SetCircleRadius(StringToDimension(attr.second));
166        return true;
167    }
168    return false;
169}
170```
171
172这个方法由框架流程调用,我们只需要在这个方法里面实现对应属性的解析,并且设置到`MyCircleComponent`中。
173
174如上代码中,入参`attr`的格式形如`<"circleradius", "40vp">`,则我们只需要判断`attr.first`为`"circleradius"`时,将`attr.second`转换为`Dimension`格式,设置到`MyCircleComponent`中即可。设置完成后,返回`true`。
175
176&nbsp;
177
178**二、组件样式的解析:`SetSpecializedStyle`**
179
180```c++
181bool DOMMyCircle::SetSpecializedStyle(const std::pair<std::string, std::string>& style)
182{
183    if (style.first == DOM_MYCIRCLE_CIRCLE_EDGE) { // "circleedge"
184        std::vector<std::string> edgeStyles;
185        // The value of [circleedge] is like "2vp red" or "2vp". To parse style value like this, we need 3 steps.
186        // Step1: Split the string value by ' ' to get vectors like ["2vp", "red"].
187        StringUtils::StringSplitter(style.second, ' ', edgeStyles);
188        Dimension edgeWidth(1, DimensionUnit::VP);
189        Color edgeColor(Color::RED);
190
191        // Step2: Parse edge color and edge width accordingly.
192        switch(edgeStyles.size()) {
193            case 0: // the value is empty
194                LOGW("Value for circle edge is empty, using default setting.");
195                break;
196            case 1: // case when only edge width is set
197                // It should be guaranteed by the tool chain when generating js-bundle that the only value is a
198                // number type for edge width rather than a color type for edge color.
199                edgeWidth = StringUtils::StringToDimension(edgeStyles[0]);
200                break;
201            case 2: // case when edge width and edge color are both set
202                edgeWidth = StringUtils::StringToDimension(edgeStyles[0]);
203                edgeColor = Color::FromString(edgeStyles[1]);
204                break;
205            default:
206                LOGW("There are more than 2 values for circle edge, please check. The value is %{private}s",
207                    style.second.c_str());
208                break;
209        }
210
211        // Step3: Set edge color and edge width to [mycircleStyle].
212        myCircleChild_->SetEdgeWidth(edgeWidth);
213        myCircleChild_->SetEdgeColor(edgeColor);
214        return true;
215    }
216    return false;
217}
218```
219
220这个方法由框架流程调用,我们只需要在这个方法里面实现对应样式的解析,并且保存到`MyCircleComponent`中。
221
222如上代码中,入参`style`的格式形如`<"circleedge", "2vp red">`,则我们只需要判断`style.first`为`"circleedge"`时,将`style.second`进行解析,设置到`MyCircleComponent`中即可。设置完成后,返回`true`。
223
224&nbsp;
225
226**三、组件事件的解析:`SetSpecializedEvent`**
227
228```c++
229bool DOMMyCircle::AddSpecializedEvent(int32_t pageId, const std::string& event)
230{
231    if (event == DOM_MYCIRCLE_CIRCLE_CLICK) { // "circleclick"
232        myCircleChild_->SetCircleClickEvent(EventMarker(GetNodeIdForEvent(), event, pageId));
233        return true;
234    }
235    return false;
236}
237```
238
239这个方法由框架流程调用,我们只需要在这个方法里面实现对应事件的解析,并且保存到`MyCircleComponent`中。
240
241如上代码中,只要判断入参`event`的值为`"circleclick"`,则我们只需要使用`eventId`和`pageId`构造一个`EventMarker`,并设置到`MyCircleComponent`中即可。设置完成后,返回`true`。
242
243&nbsp;
244
245#### 1.3 在`dom_document.cpp`里增加`mycircle`组件
246
247文件路径:`frameworks\bridge\common\dom\dom_document.cpp`
248
249```c++
250RefPtr<DOMNode> DOMDocument::CreateNodeWithId(const std::string& tag, NodeId nodeId, int32_t itemIndex)
251{
252    // code block
253    static const LinearMapNode<RefPtr<DOMNode>(*)(NodeId, const std::string&, int32_t)> domNodeCreators[] = {
254		// DomNodeCreators of other components
255		{ DOM_NODE_TAG_MENU, &DOMNodeCreator<DOMMenu> },
256		// "mycircle" must be inserted between "menu" and "navigation-bar"
257        { DOM_NODE_TAG_MYCIRCLE, &DOMNodeCreator<DOMMyCircle> },
258        { DOM_NODE_TAG_NAVIGATION_BAR, &DOMNodeCreator<DomNavigationBar> },
259		// DomNodeCreators of other components
260    };
261	// code block
262    return domNode;
263}
264```
265
266这里尤其要注意一点,`domNodeCreators[]`是一个线性表,添加`{ DOM_NODE_TAG_MYCIRCLE, &DOMNodeCreator<DOMMyCircle> }`的地方必须要符合字母序。
267
268```c++
269DOM_NODE_TAG_MENU[] = "menu",
270DOM_NODE_TAG_NAVIGATION_BAR[] = "navigation-bar",
271DOM_NODE_TAG_MYCIRCLE[] = "mycircle"
272```
273
274所以`DOM_NODE_TAG_MYCIRCLE`的记录必须添加在`"menu"`之后,`"navigation-bar"`之前。
275
276&nbsp;
277
278### 2. 后端的布局和绘制
279
280 组件在后端的布局和绘制,需要相应地新增以下几个类:`MyCircleComponent`、`MyCircleElement`、`RenderMyCircle`、`FlutterRenderMyCircle`。
281
282在后端引擎中,`Component`树、`Element`树和`Render`树为后端引擎维持和更新UI最为核心的三棵树。
283
284&nbsp;
285
286#### 2.1 新增`MyCircleComponent`类
287
288##### 2.1.1 新增`mycircle_component.h`
289
290文件路径:`frameworks\core\components\mycircle\mycircle_component.h`
291
292```c++
293class ACE_EXPORT MyCircleComponent : public RenderComponent {
294    DECLARE_ACE_TYPE(MyCircleComponent, RenderComponent);
295
296public:
297    MyCircleComponent() = default;
298    ~MyCircleComponent() override = default;
299
300    RefPtr<RenderNode> CreateRenderNode() override;
301    RefPtr<Element> CreateElement() override;
302
303    void SetCircleRadius(const Dimension& circleRadius);
304    void SetEdgeWidth(const Dimension& edgeWidth);
305    void SetEdgeColor(const Color& edgeColor);
306    void SetCircleClickEvent(const EventMarker& circleClickEvent);
307
308    const Dimension& GetCircleRadius() const;
309    const Dimension& GetEdgeWidth() const;
310    const Color& GetEdgeColor() const;
311    const EventMarker& GetCircleClickEvent() const;
312
313private:
314    Dimension circleRadius_ = 20.0_vp;
315    Dimension edgeWidth_ = 2.0_vp;
316    Color edgeColor_ = Color::RED;
317    EventMarker circleClickEvent_;
318};
319```
320
321&nbsp;
322
323##### 2.1.2 新增`mycircle_component.cpp`
324
325文件路径:`frameworks\core\components\mycircle\mycircle_component.cpp`
326
327**一、提供`Set`接口**
328
329```c++
330void MyCircleComponent::SetCircleRadius(const Dimension& circleRadius)
331{
332    circleRadius_ = circleRadius;
333}
334
335void MyCircleComponent::SetEdgeWidth(const Dimension& edgeWidth)
336{
337    edgeWidth_ = edgeWidth;
338}
339
340void MyCircleComponent::SetEdgeColor(const Color& edgeColor)
341{
342    edgeColor_ = edgeColor;
343}
344
345void MyCircleComponent::SetCircleClickEvent(const EventMarker& circleClickEvent)
346{
347    circleClickEvent_ = circleClickEvent;
348}
349```
350
351&nbsp;
352
353**二、提供`Get`接口**
354
355```c++
356const Dimension& MyCircleComponent::GetCircleRadius() const
357{
358    return circleRadius_;
359}
360
361const Dimension& MyCircleComponent::GetEdgeWidth() const
362{
363    return edgeWidth_;
364}
365
366const Color& MyCircleComponent::GetEdgeColor() const
367{
368    return edgeColor_;
369}
370
371const EventMarker& MyCircleComponent::GetCircleClickEvent() const
372{
373    return circleClickEvent_;
374}
375```
376
377&nbsp;
378
379**三、实现`CreateRenderNode`和`CreateElement`函数**
380
381```c++
382RefPtr<RenderNode> MyCircleComponent::CreateRenderNode()
383{
384    return RenderMyCircle::Create();
385}
386
387RefPtr<Element> MyCircleComponent::CreateElement()
388{
389    return AceType::MakeRefPtr<MyCircleElement>();
390}
391```
392
393&nbsp;
394
395#### 2.2 新增`MyCircleElement`类
396
397##### 2.2.1 新增`mycircle_element.h`
398
399文件路径:`frameworks\core\components\mycircle\mycircle_element.h`
400
401```c++
402class MyCircleElement : public RenderElement {
403    DECLARE_ACE_TYPE(MyCircleElement, RenderElement);
404
405public:
406    MyCircleElement() = default;
407    ~MyCircleElement() override = default;
408};
409```
410
411该组件在`element`层不涉及更多操作,只需要定义`MyCircleElement`类即可。
412
413&nbsp;
414
415#### 2.3 新增`RenderMyCircle`类
416
417##### 2.3.1 新增`render_mycircle.h`
418
419文件路径:`frameworks\core\components\mycircle\render_mycircle.h`
420
421```c++
422using CallbackForJS = std::function<void(const std::string&)>;
423
424class RenderMyCircle : public RenderNode {
425    DECLARE_ACE_TYPE(RenderMyCircle, RenderNode);
426
427public:
428    static RefPtr<RenderNode> Create();
429
430    void Update(const RefPtr<Component>& component) override;
431    void PerformLayout() override;
432    void HandleMyCircleClickEvent(const ClickInfo& info);
433
434protected:
435    RenderMyCircle();
436    void OnTouchTestHit(
437        const Offset& coordinateOffset, const TouchRestrict& touchRestrict, TouchTestResult& result) override;
438
439    Dimension circleRadius_;
440    Dimension edgeWidth_ = Dimension(1);
441    Color edgeColor_ = Color::RED;
442    CallbackForJS callbackForJS_;                   // callback for js frontend
443    RefPtr<ClickRecognizer> clickRecognizer_;
444};
445```
446
447&nbsp;
448
449##### 2.3.2 新增`render_mycircle.cpp`
450
451文件路径:`frameworks\core\components\mycircle\render_mycircle.cpp`
452
453**一、处理点击事件**
454
455```c++
456RenderMyCircle::RenderMyCircle()
457{
458    clickRecognizer_ = AceType::MakeRefPtr<ClickRecognizer>();
459    clickRecognizer_->SetOnClick([wp = WeakClaim(this)](const ClickInfo& info) {
460        auto myCircle = wp.Upgrade();
461        if (!myCircle) {
462            LOGE("WeakPtr of RenderMyCircle fails to be upgraded, stop handling click event.");
463            return;
464        }
465        myCircle->HandleMyCircleClickEvent(info);
466    });
467}
468
469void RenderMyCircle::OnTouchTestHit(
470    const Offset& coordinateOffset, const TouchRestrict& touchRestrict, TouchTestResult& result)
471{
472    clickRecognizer_->SetCoordinateOffset(coordinateOffset);
473    result.emplace_back(clickRecognizer_);
474}
475
476void RenderMyCircle::HandleMyCircleClickEvent(const ClickInfo& info)
477{
478    if (callbackForJS_) {
479        auto result = std::string("\"circleclick\",{\"radius\":")
480                    .append(std::to_string(NormalizeToPx(circleRadius_)))
481                    .append(",\"edgewidth\":")
482                    .append(std::to_string(NormalizeToPx(edgeWidth_)))
483                    .append("}");
484        callbackForJS_(result);
485    }
486}
487```
488
4891、创建一个`ClickRecognizer`;
490
4912、重写`OnTouchTestHit`函数,注册`RenderMyCircle`的`ClickRecognizer`,这样在接收到点击事件时即可触发创建`ClickRecognizer`时添加的事件回调;
492
4933、实现在接收到点击事件之后的处理逻辑`HandleMyCircleClickEvent`
494
495&nbsp;
496
497**二、重写`Update`函数**
498
499```c++
500void RenderMyCircle::Update(const RefPtr<Component>& component)
501{
502    const auto& myCircleComponent = AceType::DynamicCast<MyCircleComponent>(component);
503    if (!myCircleComponent) {
504        LOGE("MyCircleComponent is null!");
505        return;
506    }
507    circleRadius_ = myCircleComponent->GetCircleRadius();
508    edgeWidth_ = myCircleComponent->GetEdgeWidth();
509    edgeColor_ = myCircleComponent->GetEdgeColor();
510    callbackForJS_ =
511        AceAsyncEvent<void(const std::string&)>::Create(myCircleComponent->GetCircleClickEvent(), context_);
512
513    // call [MarkNeedLayout] to do [PerformLayout] with new params
514    MarkNeedLayout();
515}
516```
517
518`Update`函数负责从`MyCircleComponent`获取所有绘制、布局和事件相关的属性更新。
519
520&nbsp;
521
522**三、重写`PerformLayout`函数**
523
524```c++
525void RenderMyCircle::PerformLayout()
526{
527    double realSize = NormalizeToPx(edgeWidth_) + 2 * NormalizeToPx(circleRadius_);
528    Size layoutSizeAfterConstrain = GetLayoutParam().Constrain(Size(realSize, realSize));
529    SetLayoutSize(layoutSizeAfterConstrain);
530}
531```
532
533`PerformLayout`函数负责计算布局信息,并且调用`SetLayoutSize`函数设置自己所需要的布局大小。
534
535&nbsp;
536
537#### 2.4 新增`FlutterRenderMyCircle`类
538
539##### 2.4.1 新增`flutter_render_mycircle.h`
540
541文件路径:`frameworks\core\components\mycircle\flutter_render_mycircle.h`
542
543```c++
544class FlutterRenderMyCircle final : public RenderMyCircle {
545    DECLARE_ACE_TYPE(FlutterRenderMyCircle, RenderMyCircle);
546
547public:
548    FlutterRenderMyCircle() = default;
549    ~FlutterRenderMyCircle() override = default;
550
551    void Paint(RenderContext& context, const Offset& offset) override;
552};
553```
554
555&nbsp;
556
557##### 2.4.2 新增`flutter_render_mycircle.cpp`
558
559文件路径:`frameworks\core\components\mycircle\flutter_render_mycircle.cpp`
560
561**一、实现`RenderMyCircle::Create()`函数**
562
563```c++
564RefPtr<RenderNode> RenderMyCircle::Create()
565{
566    return AceType::MakeRefPtr<FlutterRenderMyCircle>();
567}
568```
569
570`RenderMyCircle::Create()`在基类`RenderMyCircle`中定义,因为我们当前使用的是`flutter`引擎,所以在`flutter_render_mycircle.cpp`里面实现,返回在`flutter`引擎上自渲染的`FlutterRenderMyCircle`类。
571
572&nbsp;
573
574**二、重写`Paint`函数**
575
576```c++
577void FlutterRenderMyCircle::Paint(RenderContext& context, const Offset& offset)
578{
579    auto canvas = ScopedCanvas::Create(context);
580    if (!canvas) {
581        LOGE("Paint canvas is null");
582        return;
583    }
584    SkPaint skPaint;
585    skPaint.setAntiAlias(true);
586    skPaint.setStyle(SkPaint::Style::kStroke_Style);
587    skPaint.setColor(edgeColor_.GetValue());
588    skPaint.setStrokeWidth(NormalizeToPx(edgeWidth_));
589
590    auto paintRadius = GetLayoutSize().Width() / 2.0;
591    canvas->canvas()->drawCircle(offset.GetX() + paintRadius, offset.GetY() + paintRadius,
592        NormalizeToPx(circleRadius_), skPaint);
593}
594```
595
596`Paint`函数负责调用canvas相应接口去进行绘制,这一步可以认为是新增组件的最后一步,直接决定在屏幕上绘制什么样的UI界面。
597
598&nbsp;
599
600### 小结
601
602到这里,新增一个`MyCircle`组件所需的所有步骤都已经完成,我们可以展示一个圆,支持设置半径、边缘宽度和边缘颜色,可以通过点击事件获得当前圆的半径和边缘宽度。
603
604当然`MyCircle`组件是比较简单的示例组件,JS UI开发框架支持更多更复杂的组件开发,比如提供单行文本输入组件`TextInput`、提供日历展示的`Calendar`组件等,更多的用法期待你来探索~
605
606