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 21无 22 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 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 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 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 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 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 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 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 277 278### 2. 后端的布局和绘制 279 280 组件在后端的布局和绘制,需要相应地新增以下几个类:`MyCircleComponent`、`MyCircleElement`、`RenderMyCircle`、`FlutterRenderMyCircle`。 281 282在后端引擎中,`Component`树、`Element`树和`Render`树为后端引擎维持和更新UI最为核心的三棵树。 283 284 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 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 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 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 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 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 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 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 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 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 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 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 599 600### 小结 601 602到这里,新增一个`MyCircle`组件所需的所有步骤都已经完成,我们可以展示一个圆,支持设置半径、边缘宽度和边缘颜色,可以通过点击事件获得当前圆的半径和边缘宽度。 603 604当然`MyCircle`组件是比较简单的示例组件,JS UI开发框架支持更多更复杂的组件开发,比如提供单行文本输入组件`TextInput`、提供日历展示的`Calendar`组件等,更多的用法期待你来探索~ 605 606