HarmonyOS 应用开发-ArkUI事件机制

作者: 迪士尼在逃程序员 | 来源:发表于2024-08-15 15:32 被阅读0次

ArkUI提供了事件机制,这些事件提供了不同的信息用于处理程序交互逻辑,ArKUI事件按照功能来讲,可以分为以下几种:

  • 点击事件
  • 触摸事件
  • 挂载卸载事件
  • 拖拽事件
  • 按键事件
  • 焦点事件
  • 鼠标事件
  • 组件区域变化事件
  • 组件可见区域变化事件
  • 组件快捷键事件

按照事件触发后是否向父节点传递,事件可以分为:

  • 冒泡事件:是当一个组件上的事件被触发后,该事件向父节点传递。
  • 非冒泡事件:是当一个组件上的事件被触发后,该事件不会向父节点传递。

示例讲解

下面以组件区域变化事件为例,结合代码进行分析。

组件区域变化事件介绍

当UI组件大小、位置发生变化时触发该事件,应用可以通过该事件箭头UI组件的大小位置变化,该事件属于非冒泡事件。

示例代码

// xxx.ets
@Entry
@Component
struct AreaExample {
  @State value: string = 'Text'
  @State sizeValue: string = ''

  build() {
    Column() {
      Text(this.value)
        .backgroundColor(Color.Green).margin(30).fontSize(20)
        .onClick(() => {
          this.value = this.value + 'Text'
        })
        .onAreaChange((oldValue: Area, newValue: Area) => {
          console.info(`Ace: on area change, oldValue is ${JSON.stringify(oldValue)} value is ${JSON.stringify(newValue)}`)
          this.sizeValue = JSON.stringify(newValue)
        })
      Text('new area is: \n' + this.sizeValue).margin({ right: 30, left: 30 })
    }
    .width('100%').height('100%').margin({ top: 30 })
  }
}

示例效果:

每次点击Text框,Text组件区域大小都发生变化,每次将位置信息打印到控制台。

在上述示例代码中,Text组件通过绑定onAreaChange实现了对组件区域变化事件的监听,onAreaChange事件的入参位一个回调函数,该回调函数在Text组件区域发生变化时进行调用。

代码实现原理分析

1. 编译后生成的JS代码

"use strict";
class AreaExample extends ViewPU {
    constructor(parent, params, __localStorage, elmtId = -1) {
        super(parent, __localStorage, elmtId);
        this.__value = new ObservedPropertySimplePU('Text', this, "value");
        this.__sizeValue = new ObservedPropertySimplePU('', this, "sizeValue");
        this.setInitiallyProvidedValue(params);
    }
    setInitiallyProvidedValue(params) {
        if (params.value !== undefined) {
            this.value = params.value;
        }
        if (params.sizeValue !== undefined) {
            this.sizeValue = params.sizeValue;
        }
    }
    updateStateVars(params) {
    }
    purgeVariableDependenciesOnElmtId(rmElmtId) {
        this.__value.purgeDependencyOnElmtId(rmElmtId);
        this.__sizeValue.purgeDependencyOnElmtId(rmElmtId);
    }
    aboutToBeDeleted() {
        this.__value.aboutToBeDeleted();
        this.__sizeValue.aboutToBeDeleted();
        SubscriberManager.Get().delete(this.id__());
        this.aboutToBeDeletedInternal();
    }
    get value() {
        return this.__value.get();
    }
    set value(newValue) {
        this.__value.set(newValue);
    }
    get sizeValue() {
        return this.__sizeValue.get();
    }
    set sizeValue(newValue) {
        this.__sizeValue.set(newValue);
    }
    initialRender() {
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            Column.create();
            Column.width('100%');
            Column.height('100%');
            Column.margin({ top: 30 });
            if (!isInitialRender) {
                Column.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            Text.create(this.value);
            Text.backgroundColor(Color.Green);
            Text.margin(30);
            Text.fontSize(20);
            Text.onClick(() => {
                this.value = this.value + 'Text';
            });
            Text.onAreaChange((oldValue, newValue) => {
                console.info(`Ace: on area change, oldValue is ${JSON.stringify(oldValue)} value is ${JSON.stringify(newValue)}`);
                this.sizeValue = JSON.stringify(newValue);
            });
            if (!isInitialRender) {
                Text.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });
        Text.pop();
        this.observeComponentCreation((elmtId, isInitialRender) => {
            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
            Text.create('new area is: \n' + this.sizeValue);
            Text.margin({ right: 30, left: 30 });
            if (!isInitialRender) {
                Text.pop();
            }
            ViewStackProcessor.StopGetAccessRecording();
        });
        Text.pop();
        Column.pop();
    }
    rerender() {
        this.updateDirtyElements();
    }
}
ViewStackProcessor.StartGetAccessRecordingFor(ViewStackProcessor.AllocateNewElmetIdForNextComponent());
loadDocument(new AreaExample(undefined, {}));
ViewStackProcessor.StopGetAccessRecording();
//# sourceMappingURL=Index.js.map

代码分为两部分,一部分为定义了AreaExample的类,第二部分是js的入口执行程序,详细内容参考ArkUI框架学习。我们这里重点关注Text.onAreaChange方法。

2. JS与C++代码的绑定关系

Text组件对应到ace_engine后端引擎中的JSText类:


class JSText : public JSContainerBase {
public:
    static void JSBind(BindingTarget globalObj);
    static void Create(const JSCallbackInfo& info);
    static void SetWidth(const JSCallbackInfo& info);
    static void SetHeight(const JSCallbackInfo& info);
    static void SetFont(const JSCallbackInfo& info);
    static void GetFontInfo(const JSCallbackInfo& info, Font& font);
    static void SetFontSize(const JSCallbackInfo& info);
    static void SetFontWeight(const std::string& value);
    static void SetTextColor(const JSCallbackInfo& info);
    static void SetTextShadow(const JSCallbackInfo& info);
    static void SetTextOverflow(const JSCallbackInfo& info);
    static void SetMaxLines(const JSCallbackInfo& info);
    static void SetTextIndent(const JSCallbackInfo& info);
    static void SetFontStyle(int32_t value);
    static void SetAlign(const JSCallbackInfo& info);
    static void SetTextAlign(int32_t value);
    static void SetLineHeight(const JSCallbackInfo& info);
    static void SetFontFamily(const JSCallbackInfo& info);
    static void SetMinFontSize(const JSCallbackInfo& info);
    static void SetMaxFontSize(const JSCallbackInfo& info);
    static void SetLetterSpacing(const JSCallbackInfo& info);
    static void SetTextCase(int32_t value);
    static void SetBaselineOffset(const JSCallbackInfo& info);
    static void SetDecoration(const JSCallbackInfo& info);
    static void SetCopyOption(const JSCallbackInfo& info);
    static void SetHeightAdaptivePolicy(int32_t value);
    static void JsOnClick(const JSCallbackInfo& info);
    static void JsRemoteMessage(const JSCallbackInfo& info);
    static void JsOnDragStart(const JSCallbackInfo& info);
    static void JsOnDragEnter(const JSCallbackInfo& info);
    static void JsOnDragMove(const JSCallbackInfo& info);
    static void JsOnDragLeave(const JSCallbackInfo& info);
    static void JsOnDrop(const JSCallbackInfo& info);
    static void JsFocusable(const JSCallbackInfo& info);
    static void JsDraggable(const JSCallbackInfo& info);
    static void JsMenuOptionsExtension(const JSCallbackInfo& info);

private:
    static RefPtr<TextComponentV2> GetComponent();
};

查看代码发现JSText中未直接定义onAreaChange方法,在JSText::bind中也未绑定相关的方法,结合onAreaChange方法不止在Text组件中使用,也可以在其他组件中使用,因此继续往JSText继承的基类中找。

class JSContainerBase : public JSViewAbstract, public JSInteractableView {
public:
    static void Pop();
    static void JSBind(BindingTarget globalObj);
};

继续往上层找,发现在JSViewAbstract中定义了JsOnAreaChange方法:

class JSViewAbstract {
    // 忽略掉其他不相关代码片段
     static void JsOnAreaChange(const JSCallbackInfo& info);
}

在 JSViewAbstract::JSBind中实现了JS代码与C++代码的绑定关系

void JSViewAbstract::JSBind(BindingTarget globalObj)
{
    JSClass<JSViewAbstract>::Declare("JSViewAbstract");
    
    JSClass<JSViewAbstract>::StaticMethod("onAreaChange", &JSViewAbstract::JsOnAreaChange);

}

3. ArkTS onAreaChange接口定义

接口在interface/sdk-js/api/@internal/component/ets/common.d.ts中进行定义,两个参数,一个位变化前的组件区域信息,另一个为变化后的组件区域信息。

/**
* This callback is triggered when the size or position of this component change finished.
*
* @param { function } event event callback.
* @returns { T }
* @syscap SystemCapability.ArkUI.ArkUI.Full
* @crossplatform
* @since 10
*/
onAreaChange(event: (oldValue: Area, newValue: Area) => void): T;

4.前端回调函数保存逻辑

JSViewAbstract::JsOnAreaChange代码实现:

void JSViewAbstract::JsOnAreaChange(const JSCallbackInfo& info)
{
    if (info[0]->IsUndefined() && IsDisableEventVersion()) {
        LOGD("JsOnAreaChange callback is undefined");
        ViewAbstractModel::GetInstance()->DisableOnAreaChange();
        return;
    }
    std::vector<JSCallbackInfoType> checkList { JSCallbackInfoType::FUNCTION };
    if (!CheckJSCallbackInfo("JsOnAreaChange", info, checkList)) {
        return;
    }
    auto jsOnAreaChangeFunction = AceType::MakeRefPtr<JsOnAreaChangeFunction>(JSRef<JSFunc>::Cast(info[0]));

    auto onAreaChanged = [execCtx = info.GetExecutionContext(), func = std::move(jsOnAreaChangeFunction)](
                             const Rect& oldRect, const Offset& oldOrigin, const Rect& rect, const Offset& origin) {
        JAVASCRIPT_EXECUTION_SCOPE_WITH_CHECK(execCtx);
        ACE_SCORING_EVENT("onAreaChange");
        func->Execute(oldRect, oldOrigin, rect, origin);
    };
    ViewAbstractModel::GetInstance()->SetOnAreaChanged(std::move(onAreaChanged));
}

关键点:
JSCallbackInfo为从JS前端传过来的参数,对参数合法性进行校验后,从这里面获取到绑定到onAreaChange上的回调函数以及回调函数,然后继续往ViewAbstractModel中传。

ViewAbstractModel是一个纯虚接口类,其实现有ViewAbstractModelNG和ViewAbstractModelImpl两个类,其中ViewAbstractModelNG位新的架构下的实现类,我们重点关注这个类的实现。

void SetOnAreaChanged(
    std::function<void(const Rect& oldRect, const Offset& oldOrigin, const Rect& rect, const Offset& origin)>&&
        onAreaChanged) override
{
    auto areaChangeCallback = [areaChangeFunc = std::move(onAreaChanged)](const RectF& oldRect,
                                  const OffsetF& oldOrigin, const RectF& rect, const OffsetF& origin) {
        areaChangeFunc(Rect(oldRect.GetX(), oldRect.GetY(), oldRect.Width(), oldRect.Height()),
            Offset(oldOrigin.GetX(), oldOrigin.GetY()), Rect(rect.GetX(), rect.GetY(), rect.Width(), rect.Height()),
            Offset(origin.GetX(), origin.GetY()));
    };
    ViewAbstract::SetOnAreaChanged(std::move(areaChangeCallback));
}

这里也没有做更多的东西,继续调用ViewAbstract::SetOnAreaChanged方法。
看下ViewAbstract::SetOnAreaChanged实现:

void ViewAbstract::SetOnAreaChanged(
    std::function<void(const RectF& oldRect, const OffsetF& oldOrigin, const RectF& rect, const OffsetF& origin)>&&
        onAreaChanged)
{   
    auto pipeline = PipelineContext::GetCurrentContext();
    CHECK_NULL_VOID(pipeline);
    auto frameNode = ViewStackProcessor::GetInstance()->GetMainFrameNode();
    CHECK_NULL_VOID(frameNode);
    frameNode->SetOnAreaChangeCallback(std::move(onAreaChanged));
    pipeline->AddOnAreaChangeNode(frameNode->GetId());
}

这里是关键点:

  1. 通过ViewStackProcessor获取到当前正在处理的frameNode,将前端一路传递过来的回调函数保存到frameNode中。
  2. 将frameNode的ID加入到渲染管线PipeContext中保存。
// FrameNode中保存回调函数,将回调函数保存到事件总线eventHub_
void FrameNode::SetOnAreaChangeCallback(OnAreaChangedFunc&& callback)
{
    if (!lastFrameRect_) {
        lastFrameRect_ = std::make_unique<RectF>();
    }
    if (!lastParentOffsetToWindow_) {
        lastParentOffsetToWindow_ = std::make_unique<OffsetF>();
    }
    eventHub_->SetOnAreaChanged(std::move(callback));
}

// 事件总线中保存回调函数
void EventHub::SetOnAreaChanged(OnAreaChangedFunc&& onAreaChanged)
{
    onAreaChanged_ = std::move(onAreaChanged);
}

//管线中保存FrameNode
void PipelineContext::AddOnAreaChangeNode(int32_t nodeId)
{
    onAreaChangeNodeIds_.emplace(nodeId);
}

6.回调函数调用处理

从上面的分析看,最终回调函数是保存在EventHub中的onAreaChanged_,那么只需要根据onAreaChanged_在哪里调用,然后再一路查看调用点,就可以看到详细的调用栈,这里不再贴详细的代码,直接贴出来最终的调用链:

PipelineContext::FlushVsync->PipelineContext::HandleOnAreaChangeEvent->FrameNode::TriggerOnAreaChangeCallback->EventHub::FireOnAreaChanged

从调用栈可以看到,在接受到底层vsync事件时,会触发PipelineContext::FlushVsync,之后按照上述的调用链,最终调用到前台绑定到Text.onAreaChange上的回调函数。

7. 代码分析总结

整体的类图关系如下:

写在最后

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙

  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
  • 想要获取更多完整鸿蒙最新学习知识点,请移步前往小编:https://gitee.com/MNxiaona/733GH/blob/master/jianshu

相关文章

网友评论

    本文标题:HarmonyOS 应用开发-ArkUI事件机制

    本文链接:https://www.haomeiwen.com/subject/jkqtxjtx.html