arkui_ace_engine/如何新增一个组件.md
2021-06-29 02:38:51 +00:00

606 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# JS UI开发框架新增组件开发指南以新增MyCircle组件为例
本篇wiki将通过新增一个MyCircle组件为例向大家展示新增一个JS UI组件的全流程。
完整的patch链接https://gitee.com/theretherehuh/ace_ace_engine/pulls/1/files
### mycircle
可点击的展示类组件,展示一个圆,支持设置半径、边缘宽度和边缘颜色,可以通过点击事件获得当前圆的半径和边缘宽度。
#### 支持设备
| 手机 | 智慧屏 | 智能穿戴 | 轻量级智能穿戴 | 轻车机 |
| ---- | ------ | -------- | -------------- | ------ |
| 支持 | 支持 | 支持 | 支持 | 支持 |
#### 子组件
#### 属性
| 名称 | 属性类型 | 默认值 | 必填 | 描述 |
| ------------ | -------- | ------ | ---- | ---------- |
| circleradius | length | 20vp | 否 | 默认半径。 |
#### 样式
| 名称 | 属性类型 | 默认值 | 必填 | 描述 |
| ---------- | ------------ | ------- | ---- | -------------------- |
| circleedge | length color | 2vp red | 否 | 默认边缘颜色和宽度。 |
#### 事件
| 名称 | 参数类型 | 描述 |
| ----------- | ----------------------------------------------------- | ------------------------------------------------------------ |
| circleclick | {radius: circle radius, edgewidth: circle edge width} | 点击MyCircle组件时触发该回调返回当前circle的半径和边缘宽度单位是px。 |
#### 示例
```typescript
<!-- xxx.hml -->
<div style="flex-direction: column;align-items: center;">
<text>"MyCircle的半径为{{radiusOfMyCircle}}"</text>
<text>"MyCircle的边缘宽度为{{edgeWidthOfMyCircle}}"</text>
<mycircle circleradius="40vp" style="circleedge: 2vp red;" @circleclick="onCircleClick"> </mycircle>
</div>
```
```js
// xxx.js
export default{
data:{
radiusOfMyCircle: -1,
edgeWidthOfMyCircle: -1,
},
onCircleClick(event) {
this.radiusOfMyCircle = event.radius
this.edgeWidthOfMyCircle = event.edgewidth
}
}
```
该界面最终效果如下图所示:
![](https://gitee.com/theretherehuh/ace_ace_engine/raw/resources/mycircle.gif)
### 1. `js`的界面解析
#### 1.1 `dom_type`中增加新组件的属性定义
##### 1.1.1 在`dom_type.h`中增加`MyCircle`的属性定义
文件路径为:`frameworks\bridge\common\dom\dom_type.h`
```c++
// node tag defines
/* .................................... */
/* node tag defines of other components */
/* .................................... */
ACE_EXPORT extern const char DOM_NODE_TAG_MYCIRCLE[];
/* ........................... */
/* defines of other components */
/* ........................... */
// mycircle defines
ACE_EXPORT extern const char DOM_MYCIRCLE_CIRCLE_EDGE[];
ACE_EXPORT extern const char DOM_MYCIRCLE_CIRCLE_RADIUS[];
ACE_EXPORT extern const char DOM_MYCIRCLE_CIRCLE_CLICK[];
```
&nbsp;
##### 1.1.2 在`dom_type.cpp`中增加`MyCircle`的属性值
文件路径为:`frameworks\bridge\common\dom\dom_type.cpp`
```c++
// node tag defines
/* .................................... */
/* node tag defines of other components */
/* .................................... */
const char DOM_NODE_TAG_MYCIRCLE[] = "mycircle";
/* ........................... */
/* defines of other components */
/* ........................... */
// mycircle defines
const char DOM_MYCIRCLE_CIRCLE_EDGE[] = "circleedge";
const char DOM_MYCIRCLE_CIRCLE_RADIUS[] = "circleradius";
const char DOM_MYCIRCLE_CIRCLE_CLICK[] = "circleclick";
```
&nbsp;
#### 1.2 新增`DOMMyCircle`类
##### 1.2.1 新增`dom_mycircle.h`
文件路径:`frameworks\bridge\common\dom\dom_mycircle.h`
```c++
class DOMMyCircle final : public DOMNode {
DECLARE_ACE_TYPE(DOMMyCircle, DOMNode);
public:
DOMMyCircle(NodeId nodeId, const std::string& nodeName);
~DOMMyCircle() override = default;
RefPtr<Component> GetSpecializedComponent() override
{
return myCircleChild_;
}
protected:
bool SetSpecializedAttr(const std::pair<std::string, std::string>& attr) override;
bool SetSpecializedStyle(const std::pair<std::string, std::string>& style) override;
bool AddSpecializedEvent(int32_t pageId, const std::string& event) override;
private:
RefPtr<MyCircleComponent> myCircleChild_;
};
```
`DOMMyCircle`继承自`DOMNode`,主要功能是解析界面并生成相应的`Component`节点。
&nbsp;
##### 1.2.2 新增`dom_mycircle.cpp`
文件路径:`frameworks\bridge\common\dom\dom_mycircle.cpp`
**一、组件属性的解析:`SetSpecializedAttr`**
```c++
bool DOMMyCircle::SetSpecializedAttr(const std::pair<std::string, std::string>& attr)
{
if (attr.first == DOM_MYCIRCLE_CIRCLE_RADIUS) { // "circleradius"
myCircleChild_->SetCircleRadius(StringToDimension(attr.second));
return true;
}
return false;
}
```
这个方法由框架流程调用,我们只需要在这个方法里面实现对应属性的解析,并且设置到`MyCircleComponent`中。
如上代码中,入参`attr`的格式形如`<"circleradius" "40vp">`,则我们只需要判断`attr.first`为`"circleradius"`时,将`attr.second`转换为`Dimension`格式,设置到`MyCircleComponent`中即可。设置完成后,返回`true`。
&nbsp;
**二、组件样式的解析:`SetSpecializedStyle`**
```c++
bool DOMMyCircle::SetSpecializedStyle(const std::pair<std::string, std::string>& style)
{
if (style.first == DOM_MYCIRCLE_CIRCLE_EDGE) { // "circleedge"
std::vector<std::string> edgeStyles;
// The value of [circleedge] is like "2vp red" or "2vp". To parse style value like this, we need 3 steps.
// Step1: Split the string value by ' ' to get vectors like ["2vp", "red"].
StringUtils::StringSpliter(style.second, ' ', edgeStyles);
Dimension edgeWidth(1, DimensionUnit::VP);
Color edgeColor(Color::RED);
// Step2: Parse edge color and edge width accordingly.
switch(edgeStyles.size()) {
case 0: // the value is empty
LOGW("Value for circle edge is empty, using default setting.");
break;
case 1: // case when only edge width is set
// It should be guaranteed by the tool chain when generating js-bundle that the only value is a
// number type for edge width rather than a color type for edge color.
edgeWidth = StringUtils::StringToDimension(edgeStyles[0]);
break;
case 2: // case when edge width and edge color are both set
edgeWidth = StringUtils::StringToDimension(edgeStyles[0]);
edgeColor = Color::FromString(edgeStyles[1]);
break;
default:
LOGW("There are more than 2 values for circle edge, please check. The value is %{private}s",
style.second.c_str());
break;
}
// Step3: Set edge color and edge width to [mycircleStyle].
myCircleChild_->SetEdgeWidth(edgeWidth);
myCircleChild_->SetEdgeColor(edgeColor);
return true;
}
return false;
}
```
这个方法由框架流程调用,我们只需要在这个方法里面实现对应样式的解析,并且保存到`MyCircleComponent`中。
如上代码中,入参`style`的格式形如`<"circleedge" "2vp red">`,则我们只需要判断`style.first`为`"circleedge"`时,将`style.second`进行解析,设置到`MyCircleComponent`中即可。设置完成后,返回`true`。
&nbsp;
**三、组件事件的解析:`SetSpecializedEvent`**
```c++
bool DOMMyCircle::AddSpecializedEvent(int32_t pageId, const std::string& event)
{
if (event == DOM_MYCIRCLE_CIRCLE_CLICK) { // "circleclick"
myCircleChild_->SetCircleClickEvent(EventMarker(GetNodeIdForEvent(), event, pageId));
return true;
}
return false;
}
```
这个方法由框架流程调用,我们只需要在这个方法里面实现对应事件的解析,并且保存到`MyCircleComponent`中。
如上代码中,只要判断入参`event`的值为`"circleclick"`,则我们只需要使用`eventId`和`pageId`构造一个`EventMarker`,并设置到`MyCircleComponent`中即可。设置完成后,返回`true`。
&nbsp;
#### 1.3 在`dom_document.cpp`里增加`mycircle`组件
文件路径:`frameworks\bridge\common\dom\dom_document.cpp`
```
RefPtr<DOMNode> DOMDocument::CreateNodeWithId(const std::string& tag, NodeId nodeId, int32_t itemIndex)
{
// code block
static const LinearMapNode<RefPtr<DOMNode>(*)(NodeId, const std::string&, int32_t)> domNodeCreators[] = {
// DomNodeCreators of other components
{ DOM_NODE_TAG_MENU, &DOMNodeCreator<DOMMenu> },
// "mycircle" must be inserted between "menu" and "navigation-bar"
{ DOM_NODE_TAG_MYCIRCLE, &DOMNodeCreator<DOMMyCircle> },
{ DOM_NODE_TAG_NAVIGATION_BAR, &DOMNodeCreator<DomNavigationBar> },
// DomNodeCreators of other components
};
// code block
return domNode;
}
```
这里尤其要注意一点,`domNodeCreators[]`是一个线性表,添加`{ DOM_NODE_TAG_MYCIRCLE, &DOMNodeCreator<DOMMyCircle> }`的地方必须要符合字母序。
```c++
DOM_NODE_TAG_MENU[] = "menu",
DOM_NODE_TAG_NAVIGATION_BAR[] = "navigation-bar",
DOM_NODE_TAG_MYCIRCLE[] = "mycircle"
```
所以`DOM_NODE_TAG_MYCIRCLE`的记录必须添加在`"menu"`之后,`"navigation-bar"`之前。
&nbsp;
### 2. 后端的布局和绘制
组件在后端的布局和绘制,需要相应地新增以下几个类:`MyCircleComponent`、`MyCircleElement`、`RenderMyCircle`、`FlutterRenderMyCircle`。
在后端引擎中,`Component`树、`Element`树和`Render`树为后端引擎维持和更新UI最为核心的三棵树。
&nbsp;
#### 2.1 新增`MyCircleComponent`类
##### 2.1.1 新增`mycircle_component.h`
文件路径:`frameworks\core\components\mycircle\mycircle_component.h`
```c++
class ACE_EXPORT MyCircleComponent : public RenderComponent {
DECLARE_ACE_TYPE(MyCircleComponent, RenderComponent);
public:
MyCircleComponent() = default;
~MyCircleComponent() override = default;
RefPtr<RenderNode> CreateRenderNode() override;
RefPtr<Element> CreateElement() override;
void SetCircleRadius(const Dimension& circleRadius);
void SetEdgeWidth(const Dimension& edgeWidth);
void SetEdgeColor(const Color& edgeColor);
void SetCircleClickEvent(const EventMarker& circleClickEvent);
const Dimension& GetCircleRadius() const;
const Dimension& GetEdgeWidth() const;
const Color& GetEdgeColor() const;
const EventMarker& GetCircleClickEvent() const;
private:
Dimension circleRadius_ = 20.0_vp;
Dimension edgeWidth_ = 2.0_vp;
Color edgeColor_ = Color::RED;
EventMarker circleClickEvent_;
};
```
&nbsp;
##### 2.1.2 新增`mycircle_component.cpp`
文件路径:`frameworks\core\components\mycircle\mycircle_component.cpp`
**一、提供`Set`接口**
```c++
void MyCircleComponent::SetCircleRadius(const Dimension& circleRadius)
{
circleRadius_ = circleRadius;
}
void MyCircleComponent::SetEdgeWidth(const Dimension& edgeWidth)
{
edgeWidth_ = edgeWidth;
}
void MyCircleComponent::SetEdgeColor(const Color& edgeColor)
{
edgeColor_ = edgeColor;
}
void MyCircleComponent::SetCircleClickEvent(const EventMarker& circleClickEvent)
{
circleClickEvent_ = circleClickEvent;
}
```
&nbsp;
**二、提供`Get`接口**
```c++
const Dimension& MyCircleComponent::GetCircleRadius() const
{
return circleRadius_;
}
const Dimension& MyCircleComponent::GetEdgeWidth() const
{
return edgeWidth_;
}
const Color& MyCircleComponent::GetEdgeColor() const
{
return edgeColor_;
}
const EventMarker& MyCircleComponent::GetCircleClickEvent() const
{
return circleClickEvent_;
}
```
&nbsp;
**三、实现`CreateRenderNode`和`CreateElement`函数**
```c++
RefPtr<RenderNode> MyCircleComponent::CreateRenderNode()
{
return RenderMyCircle::Create();
}
RefPtr<Element> MyCircleComponent::CreateElement()
{
return AceType::MakeRefPtr<MyCircleElement>();
}
```
&nbsp;
#### 2.2 新增`MyCircleElement`类
##### 2.2.1 新增`mycircle_element.h`
文件路径:`frameworks\core\components\mycircle\mycircle_element.h`
```c++
class MyCircleElement : public RenderElement {
DECLARE_ACE_TYPE(MyCircleElement, RenderElement);
public:
MyCircleElement() = default;
~MyCircleElement() override = default;
};
```
该组件在`element`层不涉及更多操作,只需要定义`MyCircleElement`类即可。
&nbsp;
#### 2.3 新增`RenderMyCircle`类
##### 2.3.1 新增`render_mycircle.h`
文件路径:`frameworks\core\components\mycircle\render_mycircle.h`
```c++
using CallbackForJS = std::function<void(const std::string&)>;
class RenderMyCircle : public RenderNode {
DECLARE_ACE_TYPE(RenderMyCircle, RenderNode);
public:
static RefPtr<RenderNode> Create();
void Update(const RefPtr<Component>& component) override;
void PerformLayout() override;
void HandleMyCircleClickEvent(const ClickInfo& info);
protected:
RenderMyCircle();
void OnTouchTestHit(
const Offset& coordinateOffset, const TouchRestrict& touchRestrict, TouchTestResult& result) override;
Dimension circleRadius_;
Dimension edgeWidth_ = Dimension(1);
Color edgeColor_ = Color::RED;
CallbackForJS callbackForJS_; // callback for js frontend
RefPtr<ClickRecognizer> clickRecognizer_;
};
```
&nbsp;
##### 2.3.2 新增`render_mycircle.cpp`
文件路径:`frameworks\core\components\mycircle\render_mycircle.cpp`
**一、处理点击事件**
```c++
RenderMyCircle::RenderMyCircle()
{
clickRecognizer_ = AceType::MakeRefPtr<ClickRecognizer>();
clickRecognizer_->SetOnClick([wp = WeakClaim(this)](const ClickInfo& info) {
auto myCircle = wp.Upgrade();
if (!myCircle) {
LOGE("WeakPtr of RenderMyCircle fails to be upgraded, stop handling click event.");
return;
}
myCircle->HandleMyCircleClickEvent(info);
});
}
void RenderMyCircle::OnTouchTestHit(
const Offset& coordinateOffset, const TouchRestrict& touchRestrict, TouchTestResult& result)
{
clickRecognizer_->SetCoordinateOffset(coordinateOffset);
result.emplace_back(clickRecognizer_);
}
void RenderMyCircle::HandleMyCircleClickEvent(const ClickInfo& info)
{
if (callbackForJS_) {
auto result = std::string("\"circleclick\",{\"radius\":")
.append(std::to_string(NormalizeToPx(circleRadius_)))
.append(",\"edgewidth\":")
.append(std::to_string(NormalizeToPx(edgeWidth_)))
.append("}");
callbackForJS_(result);
}
}
```
1、创建一个`ClickRecognizer`
2、重写`OnTouchTestHit`函数,注册`RenderMyCircle`的`ClickRecognizer`,这样在接收到点击事件时即可触发创建`ClickRecognizer`时添加的事件回调;
3、实现在接收到点击事件之后的处理逻辑`HandleMyCircleClickEvent`
&nbsp;
**二、重写`Update`函数**
```c++
void RenderMyCircle::Update(const RefPtr<Component>& component)
{
const auto& myCircleComponent = AceType::DynamicCast<MyCircleComponent>(component);
if (!myCircleComponent) {
LOGE("MyCircleComponent is null!");
return;
}
circleRadius_ = myCircleComponent->GetCircleRadius();
edgeWidth_ = myCircleComponent->GetEdgeWidth();
edgeColor_ = myCircleComponent->GetEdgeColor();
callbackForJS_ =
AceAsyncEvent<void(const std::string&)>::Create(myCircleComponent->GetCircleClickEvent(), context_);
// call [MarkNeedLayout] to do [PerformLayout] with new params
MarkNeedLayout();
}
```
`Update`函数负责从`MyCircleComponent`获取所有绘制、布局和事件相关的属性更新。
&nbsp;
**三、重写`PerformLayout`函数**
```c++
void RenderMyCircle::PerformLayout()
{
double realSize = NormalizeToPx(edgeWidth_) + 2 * NormalizeToPx(circleRadius_);
Size layoutSizeAfterConstrain = GetLayoutParam().Constrain(Size(realSize, realSize));
SetLayoutSize(layoutSizeAfterConstrain);
}
```
`PerformLayout`函数负责计算布局信息,并且调用`SetLayoutSize`函数设置自己所需要的布局大小。
&nbsp;
#### 2.4 新增`FlutterRenderMyCircle`类
##### 2.4.1 新增`flutter_render_mycircle.h`
文件路径:`frameworks\core\components\mycircle\flutter_render_mycircle.h`
```c++
class FlutterRenderMyCircle final : public RenderMyCircle {
DECLARE_ACE_TYPE(FlutterRenderMyCircle, RenderMyCircle);
public:
FlutterRenderMyCircle() = default;
~FlutterRenderMyCircle() override = default;
void Paint(RenderContext& context, const Offset& offset) override;
};
```
&nbsp;
##### 2.4.2 新增`flutter_render_mycircle.cpp`
文件路径:`frameworks\core\components\mycircle\flutter_render_mycircle.cpp`
**一、实现`RenderMyCircle::Create()`函数**
```c++
RefPtr<RenderNode> RenderMyCircle::Create()
{
return AceType::MakeRefPtr<FlutterRenderMyCircle>();
}
```
`RenderMyCircle::Create()`在基类`RenderMyCircle`中定义,因为我们当前使用的是`flutter`引擎,所以在`flutter_render_mycircle.cpp`里面实现,返回在`flutter`引擎上自渲染的`FlutterRenderMyCircle`类。
&nbsp;
**二、重写`Paint`函数**
```c++
void FlutterRenderMyCircle::Paint(RenderContext& context, const Offset& offset)
{
auto canvas = ScopedCanvas::Create(context);
if (!canvas) {
LOGE("Paint canvas is null");
return;
}
SkPaint skPaint;
skPaint.setAntiAlias(true);
skPaint.setStyle(SkPaint::Style::kStroke_Style);
skPaint.setColor(edgeColor_.GetValue());
skPaint.setStrokeWidth(NormalizeToPx(edgeWidth_));
auto paintRadius = GetLayoutSize().Width() / 2.0;
canvas->canvas()->drawCircle(offset.GetX() + paintRadius, offset.GetY() + paintRadius,
NormalizeToPx(circleRadius_), skPaint);
}
```
`Paint`函数负责调用canvas相应接口去进行绘制这一步可以认为是新增组件的最后一步直接决定在屏幕上绘制什么样的UI界面。
&nbsp;
### 小结
到这里,新增一个`MyCircle`组件所需的所有步骤都已经完成,我们可以展示一个圆,支持设置半径、边缘宽度和边缘颜色,可以通过点击事件获得当前圆的半径和边缘宽度。
当然`MyCircle`组件是比较简单的示例组件JS UI开发框架支持更多更复杂的组件开发比如提供单行文本输入组件`TextInput`、提供日历展示的`Calendar`组件等,更多的用法期待你来探索~